LCOV - code coverage report
Current view: top level - ballet/chacha - fd_chacha_rng.h (source / functions) Hit Total Coverage
Test: cov.lcov Lines: 31 47 66.0 %
Date: 2026-10-22 18:10:43 Functions: 3 1040 0.3 %

          Line data    Source code
       1             : #ifndef HEADER_fd_src_ballet_chacha_fd_chacha_rng_h
       2             : #define HEADER_fd_src_ballet_chacha_fd_chacha_rng_h
       3             : 
       4             : /* fd_chacha_rng provides APIs for ChaCha-based RNG, as used in the
       5             :    Solana protocol.  This API should only be used where necessary.
       6             :    fd_rng is a better choice in all other cases. */
       7             : 
       8             : #include "fd_chacha.h"
       9             : #include "../../util/bits/fd_uwide.h"
      10             : 
      11             : /* FD_CHACHA_RNG_DEBUG controls debug logging.  0 is off; 1 is on. */
      12             : 
      13             : #ifndef FD_CHACHA_RNG_DEBUG
      14             : #define FD_CHACHA_RNG_DEBUG 0
      15             : #endif
      16             : 
      17             : /* Solana uses different mechanisms of mapping a ulong to an unbiased
      18             :    integer in [0, n) in different parts of the code.  In particular,
      19             :    leader schedule generation uses MODE_MOD and Turbine uses MODE_SHIFT.
      20             :    See the note in fd_chacha_rng_ulong_roll for more details. */
      21    20705151 : #define FD_CHACHA_RNG_MODE_MOD   1
      22           0 : #define FD_CHACHA_RNG_MODE_SHIFT 2
      23             : 
      24             : /* Solana uses different ChaCha algorithms. Leader schedule generation
      25             :    uses ChaCha20 and Turbine switched from ChaCha20 to ChaCha8. */
      26        1294 : #define FD_CHACHA_RNG_ALGO_CHACHA20   1
      27      277835 : #define FD_CHACHA_RNG_ALGO_CHACHA8    2
      28             : 
      29             : /* FD_CHACHA_RNG_BUFSZ is the internal buffer size of pre-generated
      30             :    ChaCha20 blocks.  Multiple of block size (64 bytes) and a power of 2. */
      31             : 
      32             : #if FD_HAS_AVX512
      33           0 : #define FD_CHACHA_RNG_BUFSZ (16*FD_CHACHA_BLOCK_SZ)
      34             : #elif FD_HAS_AVX
      35             : #define FD_CHACHA_RNG_BUFSZ (8*FD_CHACHA_BLOCK_SZ)
      36             : #elif FD_HAS_SVE2
      37             : #define FD_CHACHA_RNG_BUFSZ (4*FD_CHACHA_BLOCK_SZ)
      38             : #else
      39             : #define FD_CHACHA_RNG_BUFSZ (256UL)
      40             : #endif
      41             : 
      42             : struct __attribute__((aligned(32UL))) fd_chacha_rng_private {
      43             :   /* ChaCha20 encryption key */
      44             :   uchar key[ 32UL ] __attribute__((aligned(32UL)));
      45             : 
      46             :   /* Ring buffer of pre-generated ChaCha20 RNG data.
      47             :      Note: We currently assume all reads are 8 byte.  This means the
      48             :            cursor is always aligned by 8 and strictly increases in
      49             :            increments of 8.  Thus, we really only have to refill the
      50             :            buffer if buf_off==buf_fill.  pre_buf is used for one
      51             :            optimization where it's helpful to stage data before buf. */
      52             :   uchar pre_buf[ 128 ] __attribute__((aligned(FD_CHACHA_BLOCK_SZ)));
      53             :   uchar buf[ FD_CHACHA_RNG_BUFSZ ] __attribute__((aligned(FD_CHACHA_BLOCK_SZ)));
      54             :   ulong buf_off;   /* Total number of bytes consumed */
      55             :   ulong buf_fill;  /* Total number of bytes produced
      56             :                       Always aligned by FD_CHACHA_BLOCK_SZ */
      57             : 
      58             :   int mode;
      59             :   int algo;
      60             : };
      61             : typedef struct fd_chacha_rng_private fd_chacha_rng_t;
      62             : 
      63             : FD_PROTOTYPES_BEGIN
      64             : 
      65             : /* fd_chacha_rng_{align,footprint} give the needed alignment and
      66             :    footprint of a memory region suitable to hold a ChaCha20-based RNG.
      67             : 
      68             :    fd_chacha_rng_new formats a memory region with suitable alignment
      69             :    and footprint for holding a chacha20_rng object.  Assumes shmem
      70             :    points on the caller to the first byte of the memory region owned by
      71             :    the caller to use.  `mode` must be one of the FD_CHACHA_RNG_MODE_*
      72             :    constants defined above and dictates what mode this object will use
      73             :    to generate random numbers. Returns shmem on success and NULL on
      74             :    failure (logs details).  The memory region will be owned by the
      75             :    object on successful return.  The caller is not joined on return.
      76             : 
      77             :    fd_chacha_rng_join joins the caller to a chacha20_rng object.
      78             :    Assumes shrng points to the first byte of the memory region holding
      79             :    the object.  Returns a local handle to the join on success (this is
      80             :    not necessarily a simple cast of the address) and NULL on failure
      81             :    (logs details).
      82             : 
      83             :    fd_chacha_rng_leave leaves the caller's current local join to a
      84             :    ChaCha20 RNG object.  Returns a pointer to the memory region holding
      85             :    the object on success this is not necessarily a simple cast of the
      86             :    address) and NULL on failure (logs details).  The caller is not
      87             :    joined on successful return.
      88             : 
      89             :    fd_chacha_rng_delete unformats a memory region that holds a ChaCha20
      90             :    RNG object.  Assumes shrng points on the caller to the first byte of
      91             :    the memory region holding the state and that nobody is joined.
      92             :    Returns a pointer to the memory region on success and NULL on failure
      93             :    (logs details).  The caller has ownership of the memory region on
      94             :    successful return. */
      95             : 
      96             : FD_FN_CONST ulong
      97             : fd_chacha_rng_align( void );
      98             : 
      99             : FD_FN_CONST ulong
     100             : fd_chacha_rng_footprint( void );
     101             : 
     102             : void *
     103             : fd_chacha_rng_new( void * shmem, int mode );
     104             : 
     105             : fd_chacha_rng_t *
     106             : fd_chacha_rng_join( void * shrng );
     107             : 
     108             : void *
     109             : fd_chacha_rng_leave( fd_chacha_rng_t * );
     110             : 
     111             : void *
     112             : fd_chacha_rng_delete( void * shrng );
     113             : 
     114             : /* fd_chacha_rng_init starts a ChaCha{8,20} RNG stream.  rng is
     115             :    assumed to be a current local join to a chacha_rng object with no
     116             :    other concurrent operation that would modify the state while this is
     117             :    executing.  seed points to the first byte of the RNG seed byte vector
     118             :    with 32 byte size.  Any preexisting state for an in-progress or
     119             :    recently completed calculation will be discarded.  Returns rng (on
     120             :    return, rng will have the state of a new in-progress calculation).
     121             : 
     122             :    The param algo is expected to be FD_CHACHA_RNG_ALGO_CHACHA20 or
     123             :    FD_CHACHA_RNG_ALGO_CHACH8. If invalid, it defaults to chacha20.
     124             : 
     125             :    Compatible with Rust fn rand_chacha::ChaCha20Rng::from_seed
     126             :    https://docs.rs/rand_chacha/latest/rand_chacha/struct.ChaCha20Rng.html#method.from_seed
     127             :    (and ChaCha8Rng) */
     128             : 
     129             : fd_chacha_rng_t *
     130             : fd_chacha_rng_init( fd_chacha_rng_t * rng,
     131             :                     void const *      key,
     132             :                     int               algo );
     133             : 
     134             : /* The refill function.  Not part of the public API. */
     135             : 
     136             : #if FD_HAS_AVX512
     137             : void fd_chacha8_rng_refill_avx512 ( fd_chacha_rng_t * rng );
     138             : void fd_chacha20_rng_refill_avx512( fd_chacha_rng_t * rng );
     139             : #endif
     140             : 
     141             : #if FD_HAS_AVX
     142             : void fd_chacha8_rng_refill_avx ( fd_chacha_rng_t * rng );
     143             : void fd_chacha20_rng_refill_avx( fd_chacha_rng_t * rng );
     144             : #endif
     145             : 
     146             : #if FD_HAS_SVE2
     147             : void fd_chacha8_rng_refill_sve2 ( fd_chacha_rng_t * rng );
     148             : void fd_chacha20_rng_refill_sve2( fd_chacha_rng_t * rng );
     149             : #endif
     150             : 
     151             : void fd_chacha8_rng_refill_seq ( fd_chacha_rng_t * rng );
     152             : void fd_chacha20_rng_refill_seq( fd_chacha_rng_t * rng );
     153             : 
     154             : #if FD_HAS_AVX512
     155           0 : #define fd_chacha8_rng_private_refill  fd_chacha8_rng_refill_avx512
     156      277835 : #define fd_chacha20_rng_private_refill fd_chacha20_rng_refill_avx512
     157             : #elif FD_HAS_AVX
     158             : #define fd_chacha8_rng_private_refill  fd_chacha8_rng_refill_avx
     159             : #define fd_chacha20_rng_private_refill fd_chacha20_rng_refill_avx
     160             : #elif FD_HAS_SVE2
     161             : #define fd_chacha8_rng_private_refill  fd_chacha8_rng_refill_sve2
     162             : #define fd_chacha20_rng_private_refill fd_chacha20_rng_refill_sve2
     163             : #else
     164             : #define fd_chacha8_rng_private_refill  fd_chacha8_rng_refill_seq
     165             : #define fd_chacha20_rng_private_refill fd_chacha20_rng_refill_seq
     166             : #endif
     167             : 
     168             : /* fd_chacha_rng_avail returns the number of buffered bytes. */
     169             : 
     170             : FD_FN_PURE static inline ulong
     171    19054228 : fd_chacha_rng_avail( fd_chacha_rng_t const * rng ) {
     172    19054228 :   return rng->buf_fill - rng->buf_off;
     173    19054228 : }
     174             : 
     175             : /* fd_chacha_rng_ulong read a 64-bit integer in [0,2^64) from the
     176             :    RNG stream. */
     177             : 
     178             : static inline ulong
     179    18449674 : fd_chacha_rng_ulong( fd_chacha_rng_t * rng ) {
     180    18449674 :   if( FD_UNLIKELY( fd_chacha_rng_avail( rng ) < sizeof(ulong) ) ) {
     181      277188 :     if( rng->algo==FD_CHACHA_RNG_ALGO_CHACHA8 ) {
     182           0 :       fd_chacha8_rng_private_refill( rng );
     183      277188 :     } else {
     184      277188 :       fd_chacha20_rng_private_refill( rng );
     185      277188 :     }
     186      277188 :   }
     187    18449674 :   ulong x = FD_LOAD( ulong, rng->buf + (rng->buf_off % FD_CHACHA_RNG_BUFSZ) );
     188    18449674 :   rng->buf_off += 8U;
     189    18449674 :   return x;
     190    18449674 : }
     191             : 
     192             : /* fd_chacha_rng_read32 reads 32 bytes from the RNG stream into buf.
     193             :    buf may be unaligned. */
     194             : 
     195             : static inline void
     196             : fd_chacha_rng_read32( fd_chacha_rng_t * rng,
     197           0 :                       void *            buf ) {
     198           0 :   rng->buf_off = fd_ulong_align_up( rng->buf_off, 32UL );
     199           0 :   if( FD_UNLIKELY( fd_chacha_rng_avail( rng )<32UL ) ) {
     200           0 :     if( rng->algo==FD_CHACHA_RNG_ALGO_CHACHA8 ) {
     201           0 :       fd_chacha8_rng_private_refill( rng );
     202           0 :     } else {
     203           0 :       fd_chacha20_rng_private_refill( rng );
     204           0 :     }
     205           0 :   }
     206             : 
     207           0 :   fd_memcpy( buf, rng->buf + (rng->buf_off % FD_CHACHA_RNG_BUFSZ), 32UL );
     208           0 :   rng->buf_off += 32UL;
     209           0 : }
     210             : 
     211             : /* fd_chacha_rng_ulong_roll returns an uniform IID rand in [0,n)
     212             :    analogous to fd_rng_ulong_roll.  Rejection method based using
     213             :    fd_chacha_rng_ulong.
     214             : 
     215             :    Compatible with Rust type
     216             :    <rand_chacha::ChaCha20Rng as rand::Rng>::gen<rand::distributions::Uniform<u64>>()
     217             :    as of version 0.7.0 of the crate
     218             :    https://docs.rs/rand/latest/rand/distributions/struct.Uniform.html */
     219             : 
     220             : static inline ulong
     221             : fd_chacha_rng_ulong_roll( fd_chacha_rng_t * rng,
     222    20704504 :                           ulong             n ) {
     223             :   /* We use a pretty standard rejection-sampling based approach here,
     224             :      but for future reference, here's an explanation:
     225             : 
     226             :      We know that v can take 2^64 values, and so any method that maps
     227             :      each of the 2^64 values to the range directly [0, n) will not be
     228             :      uniform distribution when 2^64 is not divisible by n.  This
     229             :      motivates using rejection sampling.
     230             : 
     231             :      The most basic approach is to map v from [0, n*floor(2^64/n) ) to
     232             :      [0, n) using v%n, but that puts a modulus on the critical path.  To
     233             :      avoid that, the Rust rand crate uses a different approach: compute
     234             :      v*n/2^64, which is also in [0, n).
     235             : 
     236             :      Now the question to answer is which values to throw out.  We pick a
     237             :      large integer k such that k*n<=2^64 and map [0, k*n) -> 0, [2^64,
     238             :      2^64+k*n) -> 1, etc.  Since k*n might be 2^64 and then not fit in a
     239             :      long, we define zone=k*n-1 <= ULONG_MAX, and make the intervals
     240             :      closed instead of half-open.
     241             : 
     242             :      Here's where the mode comes in.  Depending on what method you call
     243             :      and what datatype you use, the Rust crate uses different values of
     244             :      k.  When MODE_MOD is set, we use largest possible value of k,
     245             :      namely floor(2^64/n).  You can compute zone directly as follows:
     246             :                zone  = k*n-1
     247             :                      = floor(2^64/n)*n - 1
     248             :                      = 2^64 - (2^64%n) - 1
     249             :                      = 2^64-1 - (2^64-n)%n, since n<2^64
     250             :                      = 2^64-1 - ((2^64-1)-n+1)%n
     251             :      Which is back to having a mod... But at least if n is a
     252             :      compile-time constant then the whole zone computation becomes a
     253             :      compile-time constant.
     254             : 
     255             :      When MODE_SHIFT is set, we use uses almost the largest possible
     256             :      power of two for k.  Precisely, it uses the smallest power of two
     257             :      such that k*n >= 2^63, which is the largest power of two such that
     258             :      k*n<=2^64 unless n is a power of two.  This approach eliminates the
     259             :      mod calculation but increases the expected number of samples
     260             :      required. */
     261    20704504 :   ulong const zone = fd_ulong_if( rng->mode==FD_CHACHA_RNG_MODE_MOD,
     262    20704504 :                                   ULONG_MAX - (ULONG_MAX-n+1UL)%n,
     263    20704504 :                                   (n << (63 - fd_ulong_find_msb( n ) )) - 1UL );
     264             : 
     265 >1844*10^16 :   for( int i=0; 1; i++ ) {
     266    17978610 :     ulong v = fd_chacha_rng_ulong( rng );
     267    17978610 :     ulong hi, lo;
     268    17978610 :     fd_uwide_mul( &hi, &lo, v, n );
     269             : 
     270             : #   if FD_CHACHA_RNG_DEBUG
     271             :     FD_LOG_DEBUG(( "roll (attempt %d): n=%016lx zone: %016lx v=%016lx lo=%016lx hi=%016lx", i, n, zone, v, lo, hi ));
     272             : #   else
     273    17978610 :     (void)i;
     274    17978610 : #   endif /* FD_CHACHA_RNG_DEBUG */
     275             : 
     276    19227274 :     if( FD_LIKELY( lo<=zone ) ) return hi;
     277    17978610 :   }
     278    20704504 : }
     279             : 
     280             : /* fd_chacha_rng_ulong_roll_decreasing is an optimized version of
     281             :    for( ulong i=0UL; i<cnt; i++ )
     282             :      out[i] = fd_chacha_rng_ulong_roll( rng, n_initial-i );
     283             :    cnt must be in [0, n_initial). rng==NULL is not allowed, even if
     284             :    cnt==0. */
     285             : void
     286             : fd_chacha_rng_ulong_roll_decreasing( fd_chacha_rng_t * rng,
     287             :                                      ulong             n_initial,
     288             :                                      ulong *           out,
     289             :                                      ulong             cnt );
     290             : 
     291             : FD_PROTOTYPES_END
     292             : 
     293             : #endif /* HEADER_fd_src_ballet_chacha20_fd_chacha20_rng_h */

Generated by: LCOV version 1.14