ghost-node/pallets/helpers/src/lcg.rs
Uncle Stretch 66ca7545ce
merge EVM chains by type into a single ExodusRequest during Round 7
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2026-10-21 17:25:35 +03:00

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);
}