soroban_env_host/host/
prng.rs

1use super::{declared_size::DeclaredSizeForMetering, metered_clone::MeteredContainer};
2use crate::{
3    budget::Budget,
4    crypto::{chacha20_fill_bytes, unbias_prng_seed},
5    host::metered_clone::MeteredClone,
6    host_object::HostVec,
7    xdr::{ContractCostType, ScBytes, ScErrorCode, ScErrorType},
8    HostError,
9};
10use rand::{distributions::Uniform, prelude::Distribution, seq::SliceRandom, RngCore};
11use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng};
12use std::ops::RangeInclusive;
13
14/// PRNG subsystem in the host, which provides best-effort pseudo-randomness to
15/// guest contracts using a combination of features that guests cannot easily
16/// implement themselves:
17///
18///   - The host itself maintains one "base" PRNG which should be seeded by the
19///     embedding environment and should be different for each host instance, or
20///     roughly "each transaction in a block of transactions" so that
21///     transactions from different submitters cannot guess one another's seeds.
22///     It is the embedder's responsibility to set this to something hard to
23///     predict. In the stellar-core embedding, S is set to the combination of
24///     the txset hash and the apply-order _position_ of the transaction in the
25///     txset, which is itself defined in terms of an xor of each transaction
26///     hash and the previous ledger hash. While it is theoretically possible
27///     for a validator to guess or influence this, being able to do so also
28///     grants validators the ability to front-run the orderbook and is
29///     therefore already an attack vector for the whole stellar network's
30///     financial integrity. In other words: reusing it here doesn't make
31///     anything worse, we're already obliged to make transaction apply-order
32///     hard to guess or control.
33///
34///   - Each frame (which is to say: each contract invocation or sub-invocation)
35///     will get a new PRNG instance separately seeded from the host's "base"
36///     PRNG, and guest code can only access the frame's PRNG, not the "base"
37///     PRNG or those of any other frame. This doesn't eliminate _all_ attack
38///     vectors or mechanisms for misuse, but it's the best we can give the user
39///     for buiding on. In particular it means that a "random" contract will not
40///     behave the same way from one call to the next inside the same txset, nor
41///     can a caller control the seed for a "random" callee (since they can't
42///     observe the state of the "base" PRNG).
43///
44///   - Users _can_ reseed their frame-local PRNG if they want, which is a
45///     useful building block for random-commitment schemes. In particular if a
46///     contract is trying to make a "single random decision", and avoid having
47///     callers retry that decision repeatedly while aborting the transaction on
48///     any random decision the caller doesn't like, the contract can operate as
49///     a state machine like so:
50///
51///       - tx1: write commitment finalizing all inputs to "random action", plus
52///              N = current ledger and S = prng_bytes_new(32).
53///
54///       - tx2: re-read all committed values, if ledger > N, prng_reseed(S),
55///              and use PRNG to take "random" action committed-to.
56///
57///     With this design, assuming the contract does not expose any _online_
58///     method for its caller to observe the commitment it made in tx1, the
59///     caller (situated in the same transaction) won't know from its position
60///     whether it's to its advantage or not to abort tx1, so will naturally let
61///     it commit. Once the commitment is saved, it includes a "locked in"
62///     choice of seed, essentially propagating PRNG state out of a context the
63///     caller might have been able to influence its state via selective aborts
64///     (but didn't know whether to) into a context where the caller can no
65///     longer influence its state. The contract can then be re-invoked in the
66///     next ledger to load and execute the commitment, in tx2.
67///
68///   - We also include 3 building blocks for _using_ the PRNG: one basic one
69///     that just "generates a random BytesObject" (that the user can do
70///     anything they like with, including copying to guest linear memory), and
71///     two slightly more subtle but very standard operations that are easy to
72///     get wrong: an inclusive-range uniform u64 sampler and a Fisher-Yates
73///     vector shuffle. The latter is also hard to do cheaply in guest code
74///     without copying the vector into the guest and copying it back.
75///
76///   - All these PRNGs are ChaCha20: a strong, cheap, standard CSPRNG.
77///
78#[derive(Debug, Clone)]
79pub(crate) struct Prng(pub(crate) ChaCha20Rng);
80
81pub type Seed = <rand_chacha::ChaCha20Rng as rand::SeedableRng>::Seed;
82pub const SEED_BYTES: u64 = <Seed as DeclaredSizeForMetering>::DECLARED_SIZE;
83static_assertions::const_assert_eq!(SEED_BYTES, 32);
84
85impl std::hash::Hash for Prng {
86    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
87        self.0.get_seed().hash(state);
88        self.0.get_stream().hash(state);
89        self.0.get_word_pos().hash(state);
90    }
91}
92
93impl Prng {
94    fn charge_prng_bytes(&self, budget: &Budget, count: u64) -> Result<(), HostError> {
95        budget.charge(ContractCostType::ChaCha20DrawBytes, Some(count))
96    }
97
98    pub(crate) fn new_from_seed(seed: Seed, budget: &Budget) -> Result<Self, HostError> {
99        let seed = unbias_prng_seed(&seed, budget)?;
100        Ok(Self(ChaCha20Rng::from_seed(seed)))
101    }
102
103    pub(crate) fn u64_in_inclusive_range(
104        &mut self,
105        range: RangeInclusive<u64>,
106        budget: &Budget,
107    ) -> Result<u64, HostError> {
108        // rand::Uniform panics if start > end.
109        if range.start() > range.end() {
110            return Err((ScErrorType::Value, ScErrorCode::InvalidInput).into());
111        }
112
113        // We over-estimate the number of bytes drawn by a factor of 2, to
114        // account for the fact that a range sample is rejection-sampling which
115        // is expected to only do one draw but might do more than one.
116        self.charge_prng_bytes(budget, 2 * <u64 as DeclaredSizeForMetering>::DECLARED_SIZE)?;
117        let u = Uniform::from(range);
118        Ok(u.sample(&mut self.0))
119    }
120
121    pub(crate) fn vec_shuffle(
122        &mut self,
123        v: &HostVec,
124        budget: &Budget,
125    ) -> Result<HostVec, HostError> {
126        // A Fisher-Yates shuffle essentially does one call to u64_in_range for
127        // each element of the input vector, followed by an optional swap. Since
128        // u64_in_range is itself a rejection sampling operation (to avoid bias)
129        // we can't be 100% sure how many draws it'll make, but the expected
130        // number of draws is 1. To give ourselves a little more safety we'll
131        // double that number. We also give the implementation freedom to draw a
132        // 64-bit (8-byte) value per index, meaning we charge for generating 2 *
133        // 8 * len bytes.
134        let mut v2 = v.metered_clone(budget)?;
135        // We charge for both the PRNG draws and the swaps here (as "memcpys").
136        self.charge_prng_bytes(budget, 16u64.saturating_mul(v.len() as u64))?;
137        budget.charge(ContractCostType::MemCpy, Some(v.len() as u64))?;
138        v2.as_mut_slice().shuffle(&mut self.0);
139        Ok(v2)
140    }
141
142    pub(crate) fn bytes_new(&mut self, size: u32, budget: &Budget) -> Result<ScBytes, HostError> {
143        Vec::<u8>::charge_bulk_init_cpy(size as u64, budget)?;
144        let mut vec = vec![0u8; size as usize];
145        chacha20_fill_bytes(&mut self.0, vec.as_mut_slice(), budget)?;
146        Ok(ScBytes::try_from(vec)?)
147    }
148
149    pub(crate) fn sub_prng(&mut self, budget: &Budget) -> Result<Prng, HostError> {
150        let mut new_seed: Seed = [0; SEED_BYTES as usize];
151        chacha20_fill_bytes(&mut self.0, &mut new_seed, budget)?;
152        budget.charge(ContractCostType::MemCpy, Some(SEED_BYTES))?;
153        Ok(Self(ChaCha20Rng::from_seed(new_seed)))
154    }
155
156    pub(crate) fn unmetered_raw_sub_prng(&mut self) -> ChaCha20Rng {
157        let mut new_seed: Seed = [0; SEED_BYTES as usize];
158        self.0.fill_bytes(&mut new_seed);
159        ChaCha20Rng::from_seed(new_seed)
160    }
161}