stellar / rs-soroban-env

Rust environment for Soroban contracts.
Apache License 2.0
61 stars 43 forks source link

set_base_prng_seed does nothing to prng state as viewed by fn passed to with_test_contract_frame #1170

Closed leighmcculloch closed 12 months ago

leighmcculloch commented 1 year ago

What version are you using?

2674d867d7c6aa4212abab05ff30e5804ff1db90

What did you do?

Called set_base_prng_seed before using the prng inside a function passed to with_test_contract_frame.

What did you expect to see?

I expected to see calling set_base_prng_seed to change the results I get back from the prng.

What did you see instead?

No change. Setting the base prng seed appears to have no impact on contract code running inside a with_test_contract_frame.

Example

This is an example written in the SDK. The call to as_contract calls with_test_contract_frame with the given function.

#[test]
fn test_prng_seed() {
    let e = Env::default();
    e.host().set_base_prng_seed([0; 32]).unwrap();
    let id = e.register_contract(None, TestPrngContract);
    e.as_contract(&id, || {
        assert_eq!(e.prng().u64_in_range(0..=90), 78);
    });

    let e = Env::default();
    let id = e.register_contract(None, TestPrngContract);
    e.host().set_base_prng_seed([2; 32]).unwrap();
    e.as_contract(&id, || {
        assert_eq!(e.prng().u64_in_range(0..=90), 78);
    });
}
leighmcculloch commented 1 year ago

cc @anupsdf

graydon commented 1 year ago

Ok this is .. slightly strange.

  1. If I write a version of this test that draws, say, decently-large byte buffers from each PRNG, no problem, they differ.
  2. If I write a version of this test exactly as written (drawing only one number and clamping it to a tiny range) then they collide!
  3. If I expand the range from 0..90 to 0..900, they .. don't collide anymore, but they're also still surprisingly close numbers (776 and 779)
  4. if I expand the range even more (0..9000) they are still close, and "closer" as a percentage (7760 and 7786)

This is perplexing enough that I need to look into it more. It seems like there's really intense bias in at least the first byte out of the PRNG, or at least when doing uniform sampling.

graydon commented 12 months ago

Digging in a little more, I'm not sure whether this is happening in the Uniform sampler (biasing the number by doing wrong arithmetic) or if it's a phenomenon arising from bytes coming out of ChaCha itself. The bytes look kinda different in the "array of decimals" sense, but if you interpret the first 8 bytes out as a little-endian u64 (i.e. with the high byte of the u64 coming from the 8th byte out) they are uncomfortably similar-looking bit patterns (arising from bytes 220 vs. 221 in the output byte sequence):

buf1: [105, 12, 228, 36, 199, 57, 187, 220, 255, 181, 66, 167, 114, 167, 73, 136, 126, 229, 99, 124, 156, 9, 231, 42, 211, 148, 110, 234, 189, 179, 224, 119]
buf2: [236, 190, 9, 126, 102, 199, 118, 221, 202, 82, 42, 176, 45, 33, 101, 151, 75, 175, 29, 114, 10, 213, 208, 105, 207, 35, 93, 191, 15, 224, 13, 72]

u64 LE: 15905370036469238889
u64 LE: 15958161572649090796

u64 LE bits: 1101_1100_1011_1011_0011_1001_1100_0111_0010_0100_1110_0100_0000_1100_0110_1001
u64 LE bits: 1101_1101_0111_0110_1100_0111_0110_0110_0111_1110_0000_1001_1011_1110_1110_1100
graydon commented 12 months ago

Well, I've opened https://github.com/stellar/rs-soroban-env/pull/1232 which does something that seems not-entirely-unsupported by the evidence I can find. But I'm not especially certain about it. Comments welcome.