@@ -9,7 +9,7 @@ use curve25519_dalek::{
9
9
} ;
10
10
use monero_serai:: generators:: H ;
11
11
12
- /// This is the decomposed amount table containing the mandatory Pre-RCT amounts. It is use to pre-compute
12
+ /// This is the decomposed amount table containing the mandatory Pre-RCT amounts. It is used to pre-compute
13
13
/// zero commitments at runtime.
14
14
///
15
15
/// Defined at:
@@ -38,13 +38,14 @@ const ZERO_COMMITMENT_DECOMPOSED_AMOUNT: [u64; 172] = [
38
38
10000000000000000000
39
39
] ;
40
40
41
- /// Runtime initialized H generator.
41
+ /// Runtime initialized [`H`] generator.
42
42
static H_PRECOMP : LazyLock < VartimeEdwardsPrecomputation > =
43
43
LazyLock :: new ( || VartimeEdwardsPrecomputation :: new ( [ * H , ED25519_BASEPOINT_POINT ] ) ) ;
44
44
45
45
/// Runtime initialized zero commitment lookup table
46
46
///
47
- /// ASSUMPTION: This function assume that the [`ZERO_COMMITMENT_DECOMPOSED_AMOUNT`]
47
+ /// # Invariant
48
+ /// This function assumes that the [`ZERO_COMMITMENT_DECOMPOSED_AMOUNT`]
48
49
/// table is sorted.
49
50
pub static ZERO_COMMITMENT_LOOKUP_TABLE : LazyLock < [ ( u64 , EdwardsPoint ) ; 172 ] > =
50
51
LazyLock :: new ( || {
@@ -60,34 +61,41 @@ pub static ZERO_COMMITMENT_LOOKUP_TABLE: LazyLock<[(u64, EdwardsPoint); 172]> =
60
61
lookup_table
61
62
} ) ;
62
63
63
- /// This function compute the zero commitment given a specific amount.
64
+ /// This function computes the zero commitment given a specific amount.
64
65
///
65
66
/// It will first attempt to lookup into the table of known Pre-RCT value.
66
67
/// Compute it otherwise.
68
+ #[ expect( clippy:: cast_possible_truncation) ]
67
69
pub fn compute_zero_commitments ( amount : u64 ) -> EdwardsPoint {
68
- // OPTIMIZATION: We first make an arithmetic check that the value lies between valid pre-computed amounts. This permit to avoid the lookup cost if possible
69
- if amount > 10 && amount % 10 ! = 0 {
70
- return H_PRECOMP . vartime_multiscalar_mul ( [ Scalar :: from ( amount) , Scalar :: from ( 1_u8 ) ] ) ;
70
+
71
+ if amount = = 0 {
72
+ return H_PRECOMP . vartime_multiscalar_mul ( [ Scalar :: from ( amount) , Scalar :: from ( 1_u8 ) ] )
71
73
}
74
+
75
+ // OPTIMIZATION: Unlike monerod which execute a linear search across its lookup
76
+ // table (O(n)). Cuprate is making use of an arithmetic based constant time
77
+ // version (O(1)). It has been benchmarked in both hit and miss scenarios against
78
+ // a binary search lookup (O(log2(n))). To understand the following algorithm it
79
+ // is important to observe the pattern that follows the values of
80
+ // [`ZERO_COMMITMENT_DECOMPOSED_AMOUNT`].
81
+
82
+ // First obtain the logarithm base 10 of the amount. and extend it back to obtain
83
+ // the amount without its most significant digit.
84
+ let log = amount. checked_ilog10 ( ) . unwrap ( ) ;
85
+ let div = 10_u64 . pow ( log) ;
72
86
73
- // Binary search in lookup table (O(log2(n)) instead of O(n))
74
- let ( mut start, mut end) : ( usize , usize ) = ( 0 , ( ZERO_COMMITMENT_LOOKUP_TABLE . len ( ) - 1 ) ) ;
75
- while start <= end {
76
- let mid = ( start + end) / 2 ;
77
- let lookup = ZERO_COMMITMENT_LOOKUP_TABLE [ mid] . 0 ;
78
- match amount {
79
- a if a == lookup => {
80
- return ZERO_COMMITMENT_LOOKUP_TABLE [ mid] . 1 ;
81
- }
82
- a if a < lookup => {
83
- end = mid - 1 ;
84
- }
85
- _ => {
86
- start = mid + 1 ;
87
- }
88
- }
89
- }
87
+ // Extract the most significant digit.
88
+ let most_significant_digit = amount / div;
90
89
91
- // Compute the zero commitment if not found.
92
- H_PRECOMP . vartime_multiscalar_mul ( [ Scalar :: from ( amount) , Scalar :: from ( 1_u8 ) ] )
90
+ // If the *rounded* version is different than the exact amount. Then
91
+ // there aren't only trailing zeroes behind the most significant digit.
92
+ // The amount is not part of the table and can calculated apart.
93
+ if most_significant_digit * div != amount {
94
+ return H_PRECOMP . vartime_multiscalar_mul ( [ Scalar :: from ( amount) , Scalar :: ONE ] ) ;
95
+ }
96
+
97
+ // Calculating the index back by progressing within the powers of 10.
98
+ let index = ( most_significant_digit + u64:: from ( log) * 10 - ( u64:: from ( log) + 1 ) ) as usize ;
99
+
100
+ ZERO_COMMITMENT_LOOKUP_TABLE [ index] . 1
93
101
}
0 commit comments