-
Notifications
You must be signed in to change notification settings - Fork 3
Add radix-ness classifier as first step of the radix lower-bound path #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bc1cindy
wants to merge
2
commits into
payjoin:master
Choose a base branch
from
bc1cindy:radix-analyzer
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| //! Naive radix-ness analysis. | ||
|
|
||
| use std::collections::BTreeMap; | ||
|
|
||
| use super::denoms::{ | ||
| P2WPKH_DUST_SATS, binary_denoms_in_range, decimal_denoms_in_range, ternary_denoms_in_range, | ||
| }; | ||
|
|
||
| #[derive(Debug, Clone, PartialEq, Eq)] | ||
| pub struct PerSeriesAnalysis { | ||
| pub multiplicities: BTreeMap<u64, usize>, | ||
| } | ||
|
|
||
| /// Callers compose their own classification from the per-series data. | ||
| #[derive(Debug, Clone, PartialEq, Eq)] | ||
| pub struct RadixAnalysis { | ||
| pub pow2: PerSeriesAnalysis, | ||
| pub base3: PerSeriesAnalysis, | ||
| pub base10: PerSeriesAnalysis, | ||
| } | ||
|
|
||
| pub fn analyze(inputs: &[u64], outputs: &[u64]) -> RadixAnalysis { | ||
| let amounts: Vec<u64> = inputs.iter().chain(outputs).copied().collect(); | ||
| let max_amount = amounts.iter().copied().max().unwrap_or(0); | ||
| debug_assert!( | ||
| max_amount <= bitcoin::Amount::MAX_MONEY.to_sat(), | ||
| "amount exceeds bitcoin supply ceiling" | ||
| ); | ||
|
|
||
| let pow2_denoms = binary_denoms_in_range(*P2WPKH_DUST_SATS, max_amount); | ||
| let base3_denoms = ternary_denoms_in_range(*P2WPKH_DUST_SATS, max_amount); | ||
| let base10_denoms = decimal_denoms_in_range(*P2WPKH_DUST_SATS, max_amount); | ||
|
|
||
| RadixAnalysis { | ||
| pow2: count_multiplicities(&amounts, &pow2_denoms), | ||
| base3: count_multiplicities(&amounts, &base3_denoms), | ||
| base10: count_multiplicities(&amounts, &base10_denoms), | ||
| } | ||
| } | ||
|
|
||
| fn count_multiplicities(amounts: &[u64], denoms: &[u64]) -> PerSeriesAnalysis { | ||
| // denoms is sorted (BTreeSet output), so binary_search avoids HashSet alloc. | ||
| let mut multiplicities: BTreeMap<u64, usize> = BTreeMap::new(); | ||
| for &a in amounts { | ||
| if denoms.binary_search(&a).is_ok() { | ||
| *multiplicities.entry(a).or_insert(0) += 1; | ||
| } | ||
| } | ||
| PerSeriesAnalysis { multiplicities } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn empty_inputs_and_outputs() { | ||
| let a = analyze(&[], &[]); | ||
| assert!(a.pow2.multiplicities.is_empty()); | ||
| assert!(a.base3.multiplicities.is_empty()); | ||
| assert!(a.base10.multiplicities.is_empty()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn per_series_multiplicities_count_exact_denom_matches() { | ||
| // 1000, 2000 → Base10; 1024 → Pow2; 1500 → none. | ||
| let a = analyze(&[], &[1000, 1000, 2000, 1024, 1500]); | ||
| assert_eq!(a.base10.multiplicities.get(&1000), Some(&2)); | ||
| assert_eq!(a.base10.multiplicities.get(&2000), Some(&1)); | ||
| assert!(!a.base10.multiplicities.contains_key(&1024)); | ||
| assert_eq!(a.pow2.multiplicities.get(&1024), Some(&1)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn analyze_concatenates_inputs_and_outputs() { | ||
| let a = analyze(&[50_000], &[100_000]); | ||
| assert_eq!(a.base10.multiplicities.get(&50_000), Some(&1)); | ||
| assert_eq!(a.base10.multiplicities.get(&100_000), Some(&1)); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| //! Standard denomination series (Hamming-weight-1 values in bases 2, 3, 10). | ||
|
|
||
| use std::collections::BTreeSet; | ||
| use std::sync::LazyLock; | ||
|
|
||
| use bitcoin::ScriptBuf; | ||
| use bitcoin::WPubkeyHash; | ||
| use bitcoin::hashes::Hash; | ||
|
|
||
| // TODO: other script types have different dust thresholds; parameterize when we support them. | ||
| /// P2WPKH dust at the default 3 sat/vb relay feerate, from rust-bitcoin's | ||
| /// `minimal_non_dust()` (294 sats). | ||
| pub static P2WPKH_DUST_SATS: LazyLock<u64> = LazyLock::new(|| { | ||
| ScriptBuf::new_p2wpkh(&WPubkeyHash::from_byte_array([0u8; 20])) | ||
| .minimal_non_dust() | ||
| .to_sat() | ||
| }); | ||
|
|
||
| pub fn powers_in_range(b: u64, min: u64, max: u64) -> Vec<u64> { | ||
| if b < 2 { | ||
| return Vec::new(); | ||
| } | ||
| std::iter::successors(Some(1u64), |&p| p.checked_mul(b)) | ||
| .skip_while(|&p| p < min) | ||
| .take_while(|&p| p <= max) | ||
| .collect() | ||
| } | ||
|
|
||
| pub fn multiples_in_range(values: &[u64], coefficients: &[u64], min: u64, max: u64) -> Vec<u64> { | ||
| let mut s: BTreeSet<u64> = BTreeSet::new(); | ||
| for &v in values { | ||
| for &c in coefficients { | ||
| if let Some(cv) = v.checked_mul(c) | ||
| && cv >= min | ||
| && cv <= max | ||
| { | ||
| s.insert(cv); | ||
| } | ||
| } | ||
| } | ||
| s.into_iter().collect() | ||
| } | ||
|
|
||
| /// `{2^k}` series — powers of 2; `{1}` coefficient is identity. | ||
| pub fn binary_denoms_in_range(min: u64, max: u64) -> Vec<u64> { | ||
| powers_in_range(2, min, max) | ||
| } | ||
|
|
||
| /// `{1, 2}·3^k` series — 2 multiples per power of 3. | ||
| pub fn ternary_denoms_in_range(min: u64, max: u64) -> Vec<u64> { | ||
| multiples_in_range(&powers_in_range(3, min, max), &[1, 2], min, max) | ||
| } | ||
|
|
||
| /// `{1, 2, 5}·10^k` series — 3 multiples per power of 10. | ||
| pub fn decimal_denoms_in_range(min: u64, max: u64) -> Vec<u64> { | ||
| multiples_in_range(&powers_in_range(10, min, max), &[1, 2, 5], min, max) | ||
| } | ||
|
|
||
| /// Combined set. | ||
| pub fn standard_denoms_in_range(min: u64, max: u64) -> Vec<u64> { | ||
| if max == 0 || max < min { | ||
| return Vec::new(); | ||
| } | ||
| let mut s: BTreeSet<u64> = BTreeSet::new(); | ||
| s.extend(binary_denoms_in_range(min, max)); | ||
| s.extend(ternary_denoms_in_range(min, max)); | ||
| s.extend(decimal_denoms_in_range(min, max)); | ||
| s.into_iter().collect() | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn p2wpkh_dust_is_294() { | ||
| assert_eq!(*P2WPKH_DUST_SATS, 294); | ||
| } | ||
|
|
||
| #[test] | ||
| fn powers_in_range_b2_dust_to_1e8() { | ||
| let p = powers_in_range(2, 294, 100_000_000); | ||
| assert_eq!(p.first(), Some(&512)); | ||
| assert_eq!(p.last(), Some(&67_108_864)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn powers_in_range_b_below_2_is_empty() { | ||
| assert!(powers_in_range(0, 1, 100).is_empty()); | ||
| assert!(powers_in_range(1, 1, 100).is_empty()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn multiples_in_range_sorted_and_deduplicated() { | ||
| let m = multiples_in_range(&[2, 3, 6], &[1, 2, 3], 1, 100); | ||
| assert_eq!(m, vec![2, 3, 4, 6, 9, 12, 18]); | ||
| } | ||
|
|
||
| #[test] | ||
| fn binary_denoms_in_range_default_params() { | ||
| let b = binary_denoms_in_range(294, 100_000_000); | ||
| assert_eq!(b.first(), Some(&512)); | ||
| assert_eq!(b.last(), Some(&67_108_864)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn ternary_denoms_in_range_default_params() { | ||
| let t = ternary_denoms_in_range(294, 100_000_000); | ||
| assert!(t.contains(&729)); | ||
| assert!(t.contains(&1458)); | ||
| // 3^5 = 243 < dust, so 2·3^5 = 486 is dropped. | ||
| assert!(!t.contains(&486)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn decimal_denoms_in_range_default_params() { | ||
| let d = decimal_denoms_in_range(294, 100_000_000); | ||
| assert!(d.contains(&1_000)); | ||
| assert!(d.contains(&5_000)); | ||
| assert!(!d.contains(&200_000_000)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn standard_denoms_in_range_is_sorted_union_of_three_series() { | ||
| let s = standard_denoms_in_range(294, 100_000_000); | ||
| for w in s.windows(2) { | ||
| assert!(w[0] < w[1]); | ||
| } | ||
| assert!(s.contains(&512)); | ||
| assert!(s.contains(&729)); | ||
| assert!(s.contains(&1_000)); | ||
| // 3·2^k is not in any series. | ||
| assert!(!s.contains(&768)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn standard_denoms_in_range_empty_when_max_below_min() { | ||
| assert!(standard_denoms_in_range(1_000, 500).is_empty()); | ||
| assert!(standard_denoms_in_range(0, 0).is_empty()); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| //! Radix CoinJoin primitives vendored from "Small Hamming Weight Denominations | ||
| //! for CoinJoins" by Yuval Kogman (@nothingmuch): | ||
| //! <https://colab.research.google.com/drive/1We_FvfX_Ob9BapFW3X_By9vTtxUrt3pm> | ||
|
|
||
| pub mod analysis; | ||
| pub mod denoms; | ||
|
|
||
| pub use analysis::{PerSeriesAnalysis, RadixAnalysis, analyze}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.