453 lines
19 KiB
Rust
453 lines
19 KiB
Rust
use sp_runtime::traits::UniqueSaturatedInto;
|
|
|
|
macro_rules! generate_perfect_slot_mapper {
|
|
(
|
|
struct_name: $struct_name:ident,
|
|
inner_type: $inner_type:ty,
|
|
init_rng: $init_rng:expr,
|
|
golden_ratio: $golden_ratio:expr,
|
|
lcg_multiplier: $lcg_multiplier:expr,
|
|
lcg_increment: $lcg_increment:expr,
|
|
) => {
|
|
pub struct $struct_name;
|
|
|
|
impl $struct_name {
|
|
const TOTAL_BITS: usize = core::mem::size_of::<$inner_type>() * 8;
|
|
|
|
fn get_exact_iterations(items_count: usize) -> Option<usize> {
|
|
let table_size = Self::TOTAL_BITS;
|
|
|
|
if items_count <= 1 { return Some(1); }
|
|
if items_count > table_size { return None; }
|
|
|
|
let density_pct = (items_count * 100) / table_size;
|
|
|
|
match table_size {
|
|
16 => match density_pct {
|
|
0 => Some(1),
|
|
1..=5 => Some(1), // max 0 items
|
|
6..=10 => Some(1), // max 1 items
|
|
11..=15 => Some(2), // max 2 items
|
|
16..=20 => Some(2), // max 3 items
|
|
21..=25 => Some(3), // max 4 items
|
|
26..=30 => Some(3), // max 4 items
|
|
31..=35 => Some(5), // max 5 items
|
|
36..=40 => Some(8), // max 6 items
|
|
41..=45 => Some(13), // max 7 items
|
|
46..=50 => Some(24), // max 8 items
|
|
51..=55 => Some(24), // max 8 items
|
|
56..=60 => Some(49), // max 9 items
|
|
61..=65 => Some(112), // max 10 items
|
|
66..=70 => Some(301), // max 11 items
|
|
71..=75 => Some(966), // max 12 items
|
|
76..=80 => Some(966), // max 12 items
|
|
81..=85 => Some(3868), // max 13 items
|
|
86..=90 => Some(20633), // max 14 items
|
|
91..=95 => Some(165075), // max 15 items
|
|
_ => None,
|
|
},
|
|
32 => match density_pct {
|
|
0 => Some(1),
|
|
1..=5 => Some(1), // max 1 items
|
|
6..=10 => Some(2), // max 3 items
|
|
11..=15 => Some(2), // max 4 items
|
|
16..=20 => Some(4), // max 6 items
|
|
21..=25 => Some(7), // max 8 items
|
|
26..=30 => Some(9), // max 9 items
|
|
31..=35 => Some(20), // max 11 items
|
|
36..=40 => Some(31), // max 12 items
|
|
41..=45 => Some(85), // max 14 items
|
|
46..=50 => Some(287), // max 16 items
|
|
51..=55 => Some(575), // max 17 items
|
|
56..=60 => Some(2807), // max 19 items
|
|
61..=65 => Some(6912), // max 20 items
|
|
66..=70 => Some(53627), // max 22 items
|
|
71..=75 => Some(610170), // max 24 items
|
|
_ => None,
|
|
},
|
|
64 => match density_pct {
|
|
0 => Some(1),
|
|
1..=5 => Some(1), // max 3 items
|
|
6..=10 => Some(2), // max 6 items
|
|
11..=15 => Some(4), // max 9 items
|
|
16..=20 => Some(8), // max 12 items
|
|
21..=25 => Some(22), // max 16 items
|
|
26..=30 => Some(58), // max 19 items
|
|
31..=35 => Some(180), // max 22 items
|
|
36..=40 => Some(686), // max 25 items
|
|
41..=45 => Some(3285), // max 28 items
|
|
46..=50 => Some(38995), // max 32 items
|
|
51..=55 => Some(343497), // max 35 items
|
|
_ => None,
|
|
},
|
|
128 => match density_pct {
|
|
0 => Some(1),
|
|
1..=5 => Some(2), // max 6 items
|
|
6..=10 => Some(4), // max 12 items
|
|
11..=15 => Some(11), // max 19 items
|
|
16..=20 => Some(36), // max 25 items
|
|
21..=25 => Some(207), // max 32 items
|
|
26..=30 => Some(1368), // max 38 items
|
|
31..=35 => Some(13426), // max 44 items
|
|
36..=40 => Some(331105), // max 51 items
|
|
_ => None,
|
|
},
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn get_bits_metadata() -> Option<($inner_type, $inner_type)> {
|
|
let table_size = Self::TOTAL_BITS;
|
|
let bits_needed = table_size.trailing_zeros() as usize;
|
|
|
|
let bit_shift_usize = table_size.checked_sub(bits_needed)?;
|
|
let bit_shift = bit_shift_usize as $inner_type;
|
|
let bit_mask = (table_size - 1) as $inner_type;
|
|
|
|
Some((bit_shift, bit_mask))
|
|
}
|
|
|
|
pub fn get_lcg_xor_mask<V>(values: &[V]) -> Option<$inner_type>
|
|
where
|
|
V: UniqueSaturatedInto<$inner_type> + Copy,
|
|
{
|
|
let table_size = Self::TOTAL_BITS;
|
|
|
|
if values.len() > table_size { return None; }
|
|
|
|
let max_iterations = Self::get_exact_iterations(values.len())?;
|
|
let (bit_shift, bit_mask) = Self::get_bits_metadata()?;
|
|
|
|
let mut lcg_rng_state: $inner_type = $init_rng;
|
|
let mut xor_key: $inner_type = lcg_rng_state;
|
|
let mut slots_taken = [false; Self::TOTAL_BITS];
|
|
|
|
for _ in 0..max_iterations {
|
|
slots_taken.fill(false);
|
|
let mut collision_found = false;
|
|
|
|
for value in values {
|
|
let slot_index = Self::get_lcg_based_slot_index(
|
|
bit_mask,
|
|
bit_shift,
|
|
xor_key,
|
|
value
|
|
);
|
|
|
|
if slot_index >= table_size || slots_taken[slot_index] {
|
|
collision_found = true;
|
|
break;
|
|
}
|
|
|
|
slots_taken[slot_index] = true;
|
|
}
|
|
|
|
if !collision_found { return Some(xor_key); }
|
|
|
|
lcg_rng_state = lcg_rng_state
|
|
.wrapping_mul($lcg_multiplier)
|
|
.wrapping_add($lcg_increment);
|
|
|
|
xor_key = lcg_rng_state;
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn get_lcg_based_slot_index<V>(
|
|
bit_mask: $inner_type,
|
|
bit_shift: $inner_type,
|
|
xor_key: $inner_type,
|
|
value: &V
|
|
) -> usize
|
|
where
|
|
V: UniqueSaturatedInto<$inner_type> + Copy,
|
|
{
|
|
let converted_value: $inner_type = (*value).unique_saturated_into();
|
|
let knuth_hash = (converted_value ^ xor_key).wrapping_mul($golden_ratio);
|
|
|
|
let shifted_hash = knuth_hash.checked_shr(bit_shift as u32).unwrap_or(knuth_hash);
|
|
let slot_index_converted = shifted_hash & bit_mask;
|
|
|
|
slot_index_converted as usize
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
generate_perfect_slot_mapper!(
|
|
struct_name: PerfectSlotMapperU16,
|
|
inner_type: u16,
|
|
|
|
init_rng: 0x1234,
|
|
golden_ratio: 0x9E37, // Computed with: 2^16 * ( (sqrt(5) - 1) / 2)
|
|
lcg_multiplier: 25173, // Reference: Peter Grogono, Programming in Pascal, Section 4.5.
|
|
lcg_increment: 13849, // http://engr.case.edu/merat_francis/eeap282f97/labs/Random_Lab.pdf
|
|
);
|
|
|
|
generate_perfect_slot_mapper!(
|
|
struct_name: PerfectSlotMapperU32,
|
|
inner_type: u32,
|
|
|
|
init_rng: 0x12345678,
|
|
golden_ratio: 0x9E3779B9, // Computd with: 2^32 * ( (sqrt(5) - 1) / 2)
|
|
lcg_multiplier: 1664525, // Numerical Recipes `ranqd1`, Chapter 7.1,
|
|
lcg_increment: 1013904223, // An Even Quicker Generator, Eq. 7.1.6
|
|
);
|
|
|
|
generate_perfect_slot_mapper!(
|
|
struct_name: PerfectSlotMapperU64,
|
|
inner_type: u64,
|
|
|
|
init_rng: 0x123456789ABCDEF,
|
|
golden_ratio: 0x9E3779B97F4A7C15, // Taken from `splitmix64`, this generator is specifically used in PostgreSQL
|
|
lcg_multiplier: 6364136223846793005, // Newlib / Musl libc 64-bit LCG multiplier
|
|
lcg_increment: 1, // https://sourceware.org/git/?p=newlib-cygwin.git;a=blob;f=newlib/libc/stdlib/rand.c
|
|
);
|
|
|
|
generate_perfect_slot_mapper!(
|
|
struct_name: PerfectSlotMapperU128,
|
|
inner_type: u128,
|
|
|
|
init_rng: 0x123456789ABCDEF0123456789ABCDEF0,
|
|
golden_ratio: 0x9E3779B97F4A7C15F39CC0605CEDC834, // Computd with: 2^128 * ( (sqrt(5) - 1) / 2) with python
|
|
lcg_multiplier: 0x2360ED051FC65DA44385DF649FCCF645, // 2549297995355413924ULL,4865540595714422341ULL
|
|
lcg_increment: 0x5851F42D4C957F2D14057B7EF767814F, // 6364136223846793005ULL,1442695040888963407ULL
|
|
// https://github.com/imneme/pcg-cpp/blob/master/include/pcg_random.hpp
|
|
);
|
|
|
|
#[cfg(test)]
|
|
mod perfect_mapper_tests {
|
|
use super::*;
|
|
|
|
macro_rules! generate_mapper_test {
|
|
($mod_name:ident, $struct_name:ident, $init_rng:expr, $inner_type:ty) => {
|
|
mod $mod_name {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_standard_pack_20_percentages() {
|
|
const TABLE_SIZE: usize = $struct_name::TOTAL_BITS;
|
|
const ITEMS_COUNT: usize = (TABLE_SIZE * 20) / 100;
|
|
|
|
let mut values = [0 as $inner_type; TABLE_SIZE];
|
|
for i in 0..ITEMS_COUNT {
|
|
values[i] = ((i as $inner_type).wrapping_mul(999)) ^ 0xABC;
|
|
}
|
|
|
|
let filled_values = &values[0..ITEMS_COUNT];
|
|
let mask = $struct_name::get_lcg_xor_mask(filled_values);
|
|
|
|
assert!(mask.is_some());
|
|
let xor_key = mask.unwrap();
|
|
|
|
let bits_needed = TABLE_SIZE.trailing_zeros() as $inner_type;
|
|
let bit_mask = (TABLE_SIZE - 1) as $inner_type;
|
|
let bit_shift = (TABLE_SIZE as $inner_type) - bits_needed;
|
|
|
|
let mut slots = vec![false; TABLE_SIZE];
|
|
|
|
for val in filled_values {
|
|
let idx = $struct_name::get_lcg_based_slot_index(
|
|
bit_mask,
|
|
bit_shift,
|
|
xor_key,
|
|
val
|
|
);
|
|
assert!(idx < TABLE_SIZE);
|
|
assert!(!slots[idx]);
|
|
slots[idx] = true;
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_heavy_math() {
|
|
const TABLE_SIZE: usize = $struct_name::TOTAL_BITS;
|
|
|
|
let max_val = <$inner_type>::MAX;
|
|
let one_val = 1 as $inner_type;
|
|
|
|
let mut block_pattern = 0 as $inner_type;
|
|
let mut noise_pattern = 0 as $inner_type;
|
|
|
|
let total_bytes = core::mem::size_of::<$inner_type>();
|
|
let mut i = 0;
|
|
while i < total_bytes {
|
|
let byte_val = if i % 2 == 0 { 0x55 } else { 0xAA };
|
|
noise_pattern = (noise_pattern << 8) | (byte_val as $inner_type);
|
|
|
|
let block_val = if i < total_bytes / 3 {
|
|
0xEE
|
|
} else if i < (total_bytes * 2) / 3 {
|
|
0xFF
|
|
} else {
|
|
0xAA
|
|
};
|
|
block_pattern = (block_pattern << 8) | (block_val as $inner_type);
|
|
|
|
i += 1;
|
|
}
|
|
|
|
let center_bit_shift = (TABLE_SIZE / 2) as u32;
|
|
let center_bit_pattern = (1 as $inner_type)
|
|
.checked_shl(center_bit_shift)
|
|
.unwrap_or(1);
|
|
|
|
let values: [$inner_type; 6] = [
|
|
$init_rng,
|
|
noise_pattern,
|
|
max_val,
|
|
one_val,
|
|
center_bit_pattern,
|
|
block_pattern
|
|
];
|
|
|
|
let mask = $struct_name::get_lcg_xor_mask(&values);
|
|
assert!(mask.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_single_element() {
|
|
const TABLE_SIZE: usize = $struct_name::TOTAL_BITS;
|
|
|
|
let single_value = [420 as $inner_type];
|
|
let mask = $struct_name::get_lcg_xor_mask(&single_value);
|
|
assert!(mask.is_some());
|
|
|
|
let xor_key = mask.unwrap();
|
|
|
|
let bits_needed = TABLE_SIZE.trailing_zeros() as $inner_type;
|
|
let bit_mask = (TABLE_SIZE - 1) as $inner_type;
|
|
let bit_shift = (TABLE_SIZE as $inner_type) - bits_needed;
|
|
|
|
let idx = $struct_name::get_lcg_based_slot_index(
|
|
bit_mask,
|
|
bit_shift,
|
|
xor_key,
|
|
&single_value[0]
|
|
);
|
|
|
|
assert!(idx < TABLE_SIZE);
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_bounds_returns_none() {
|
|
const TABLE_SIZE: usize = $struct_name::TOTAL_BITS;
|
|
let oversized_values = [1 as $inner_type; TABLE_SIZE];
|
|
let mask = $struct_name::get_lcg_xor_mask(&oversized_values);
|
|
assert!(mask.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_top_25_evm_chains_by_tvl() {
|
|
const TABLE_SIZE: usize = $struct_name::TOTAL_BITS;
|
|
|
|
let evm_network_ids: [u64; 25] = [
|
|
1, // Ethereum Mainnet
|
|
8453, // Base
|
|
56, // BNB Smart Chain
|
|
42161, // Arbitrum One
|
|
10143, // Monad
|
|
137, // Polygon PoS
|
|
43114, // Avalanche C-Chain
|
|
10, // OP Mainnet
|
|
57073, // Ink
|
|
14, // Flare Network
|
|
196, // X Layer
|
|
25, // Cronos
|
|
5000, // Mantle
|
|
100, // Gnosis Chain
|
|
59144, // Linea
|
|
534352, // Scroll
|
|
324, // zkSync Era
|
|
146, // Sonic (Fantom)
|
|
1116, // Core
|
|
42220, // Celo
|
|
1088, // Metis
|
|
592, // Astar
|
|
1284, // Moonbeam
|
|
98865, // Plume Network
|
|
60808, // BOB (Build on Bitcoin)
|
|
];
|
|
|
|
let mask = $struct_name::get_lcg_xor_mask(&evm_network_ids);
|
|
|
|
if let Some(xor_key) = mask {
|
|
let mut slots_check = [false; TABLE_SIZE];
|
|
|
|
let bits_needed = TABLE_SIZE.trailing_zeros() as $inner_type;
|
|
let bit_mask = (TABLE_SIZE - 1) as $inner_type;
|
|
let bit_shift = (TABLE_SIZE as $inner_type) - bits_needed;
|
|
|
|
for chain_id in &evm_network_ids {
|
|
let slot_idx = $struct_name::get_lcg_based_slot_index(
|
|
bit_mask,
|
|
bit_shift,
|
|
xor_key,
|
|
chain_id
|
|
);
|
|
|
|
assert!(slot_idx < TABLE_SIZE);
|
|
assert!(!slots_check[slot_idx]);
|
|
|
|
slots_check[slot_idx] = true;
|
|
}
|
|
} else {
|
|
assert!(TABLE_SIZE <= 32);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_top_25_evm_testnet_chains() {
|
|
const TABLE_SIZE: usize = $struct_name::TOTAL_BITS;
|
|
|
|
let evm_network_ids: [u64; 10] = [
|
|
63, // Mordor (Ethereum Classic Testnet)
|
|
11155111, // Ethereum Sepolia
|
|
80002, // Polygon Amoy
|
|
421614, // Arbitrum Sepolia
|
|
84532, // Base Sepolia
|
|
11155420, // Optimism Sepolia
|
|
59141, // Linea Sepolia
|
|
534351, // Scroll Sepolia
|
|
80084, // Berachain Bartio
|
|
98864, // Plume Testnet
|
|
];
|
|
|
|
let mask = $struct_name::get_lcg_xor_mask(&evm_network_ids);
|
|
|
|
if let Some(xor_key) = mask {
|
|
let mut slots_check = [false; TABLE_SIZE];
|
|
|
|
let bits_needed = TABLE_SIZE.trailing_zeros() as $inner_type;
|
|
let bit_mask = (TABLE_SIZE - 1) as $inner_type;
|
|
let bit_shift = (TABLE_SIZE as $inner_type) - bits_needed;
|
|
|
|
for chain_id in &evm_network_ids {
|
|
let slot_idx = $struct_name::get_lcg_based_slot_index(
|
|
bit_mask,
|
|
bit_shift,
|
|
xor_key,
|
|
chain_id
|
|
);
|
|
|
|
assert!(slot_idx < TABLE_SIZE);
|
|
assert!(!slots_check[slot_idx]);
|
|
|
|
slots_check[slot_idx] = true;
|
|
}
|
|
} else {
|
|
assert!(TABLE_SIZE == 16);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
generate_mapper_test!(u16_mapper, PerfectSlotMapperU16, 0x1234, u16);
|
|
generate_mapper_test!(u32_mapper, PerfectSlotMapperU32, 0x12345678, u32);
|
|
generate_mapper_test!(u64_mapper, PerfectSlotMapperU64, 0x123456789ABCDEF, u64);
|
|
generate_mapper_test!(u128_mapper, PerfectSlotMapperU128, 0x123456789ABCDEF0123456789ABCDEF0, u128);
|
|
}
|