#Is it expected that env.prng would generate two different values when doing a cross contract call?

104 messages · Page 1 of 1 (latest)

wild moth
#

It's not entirely clear to me how prng is seeded by default. Right now I'm noticing the prng value is different between the parent and cross contract call. Is this expected?

#
#![no_std]
use soroban_sdk::{contract, contractimpl, Env, Address, Bytes};

mod prng_test {
    soroban_sdk::contractimport!(
        file = "./target/wasm32-unknown-unknown/release/prng_test.wasm"
    );
}

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {
    pub fn get_prng(env: Env) -> u64 {
        env.prng().u64_in_range(u64::MIN..=u64::MAX)
    }
    pub fn compare_prng(env: Env, contract_id: Address) -> (u64, u64) {
        let client = prng_test::Client::new(&env, &contract_id);
        let prng_a = client.get_prng(&bytes);

        let prng_b = Self::get_prng(env, bytes);

        (prng_a, prng_b)
    }
}
steep path
#

sure

#

prng is independent between contract calls

#

you shouldn't be able to observe someone else's prng state

wild moth
#

If I build and deploy that contract twice and call compare_prng the values will be different

steep path
#

it's not 'that' contract anymore, right?

#

these are two different contract instances

wild moth
#

It makes sense, was just curious. Wasn't sure how the prng was seeded by default. Obviously if I provide me own seed the values are the same.

icy rock
#

this base PRNG is seeded using a combination of the transaction set (txset) hash and the apply-order position of the transaction in that txset. then there is also a frame level

#

but read that file you'll quickly see

wild moth
#

Sure but if it's a single tx that does a cross contract call where is it pulling in the seed for that sub contract call?

icy rock
#

I read the whole file but i'm not sure i understand exactly how it works still lol but i think i understand the idea of how it works 😄

deep night
wild moth
steep path
#

I feel like there is some confusion here. base seed in tests is 0, so every contract invocation will get its own arbitrary-looking, but stable seed (derived from the main prng seed). if any invocation seeds itself, then it'll get its own sequence dependent on that seed. you shouldn't ever get the same seed for two different contract invocations, unless you use manual seeding

wild moth
#

unless you use manual seeding
I think this is the confusion for me as it seems @deep night even manual seeding will result in different values

steep path
#

that's surprising. it's also not what I see in the test

deep night
#

Two contracts deployed, execute, setting the same seed, results in same result in tests:

let e = Env::default();
let id = e.register_contract(None, TestPrngContract);
e.as_contract(&id, || {
    e.prng().seed(bytes!(
        &e,
        0x0000000000000000000000000000000000000000000000000000000000000001
    ));
    assert_eq!(e.prng().u64_in_range(0..=9), 5);
});

let e = Env::default();
let id = e.register_contract(None, TestPrngContract);
e.as_contract(&id, || {
    e.prng().seed(bytes!(
        &e,
        0x0000000000000000000000000000000000000000000000000000000000000001
    ));
    assert_eq!(e.prng().u64_in_range(0..=9), 5);
});
#

Outside of tests this would not happen.

steep path
#

why not?

deep night
#

Env will set a base seed based that's different for two contracts.

#

e.prng().seed(...) sets a sub-seed of the base seed. It doesn't override the base seed.

#

In tests it just happens that the base seed is always 0.

steep path
#

right, but seeding should affect the current invocation, right?

deep night
#

What do you mean?

#

In tests:

Contract 1:
Base Seed: 0
Sub Seed: 1
Result: 5

Contract 2:
Base Seed: 0
Sub Seed: 1
Result: 5

steep path
#

if I do seed(my_seed) anywhere, I expected to get the same results in any context (test or not test). it shouldn't affect seeds of any other contract invocations (again, in any context)

deep night
#

In deployed environment:

Contract 1:
Base Seed: 29837492734
Sub Seed: 1
Result: 9

Contract 2:
Base Seed: 78987432842
Sub Seed: 1
Result: 2

#

Each contract invocation has its own PRNG, they shouldn't share state.

steep path
#

hmm

#

this is confusing

#

why would my own seed depend on any external state?

deep night
#

The host has a single hidden “base” PRNG that is seeded by the network. The base PRNG is then used to seed separate, independent “local” PRNGs for each contract invocation. Contracts can only access (use or reseed) their local PRNG, and cannot influence any other contract instance’s local PRNGs nor the base PRNG.
https://docs.rs/soroban-sdk/20.0.0-rc2/soroban_sdk/prng/index.html

steep path
#

right, so it doesn't say anything specific about reseeding

#

and I think the behavior you have is incorrect or at least confusing

deep night
#

Contracts can only access (use or reseed) their local PRNG
^ word reseed is here

steep path
#

the whole point of reseeding is to get deterministic and predictable results in the the future

#

(or maybe I don't understand your example)

deep night
#

My point is base seed is 0 in tests for all contracts.

My understanding is base seed is not set to 0, but something chosen by the nominating validator.

steep path
#

sure

#

but reseeding has nothing to do with the base seed

#

again, the whole point of reseeding is to not depend on the base seed

deep night
#

So are you saying that when a contact sets a new seed, it means the contract is not influenced by the base seed any longer?

steep path
#

that would be my expectation

#

otherwise what's the point of reseeding?

deep night
#

But is that what it does? The docs don't seem to say that, at least not as I read them.

steep path
#

the docs just say that the prng can be reseeded

deep night
#

The docs say only the local PRNG can be reseeded.

steep path
#

in any rng library I've dealt with that means that it will result in a well-defined sequence of generated numbers

#

sure

deep night
#

I'm not sure if that means the Local PRNG is affected by the base seed any longer.

steep path
#

but if local prng A is reseeded to seed S, and local prng B is reseeded to seed S, I would expect them to produce exactly the same bit sequences

deep night
#

I thought yes (base seed always affects) from conversations with @hybrid karma. But I might have misunderstood.

hybrid karma
#

(I designed and wrote this code so I'm happy to answer questions!)

icy rock
hybrid karma
#

there's a lot of questions going on here. I'm trying to untangle...

deep night
#

I think what we need to understand is when a contract sets a new seed for its local PRNG, is it affected by the base seed still?

hybrid karma
#

no, it is not

deep night
#

Ok thanks, that's where my misunderstanding was about.

steep path
#

ok, then I'm still really confused by this:

Contract 1:
Base Seed: 29837492734
Sub Seed: 1
Result: 9

Contract 2:
Base Seed: 78987432842
Sub Seed: 1
Result: 2
hybrid karma
#

when you write "sub seed" what do you mean?

icy rock
deep night
#

So I think the behavior that @steep path was describing is correct.

icy rock
#

Oops not the link but the link in that msg sorry

deep night
#

And this test demonstrates that behavior pretty clearly:

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, || {
    e.prng().seed(bytes!(
          &e,
          0x0000000000000000000000000000000000000000000000000000000000000001
      ));
      assert_eq!(e.prng().u64_in_range(0..=9), 5);
  });

  let e = Env::default();
  let id = e.register_contract(None, TestPrngContract);
  e.host().set_base_prng_seed([1; 32]).unwrap();
  e.as_contract(&id, || {
      e.prng().seed(bytes!(
          &e,
          0x0000000000000000000000000000000000000000000000000000000000000001
      ));
      assert_eq!(e.prng().u64_in_range(0..=9), 5);
  });
}```
hybrid karma
#

yes, that's what I'd expect to have happen. are you not observing it somewhere?

deep night
#

No, I was just wrong.

hybrid karma
#

(that is a question for @steep path )

steep path
#

that's the Leigh's example - IDK really how it was produced

#

I thought that 'sub-seed' is the value we reseed local prng to

deep night
#

We don't need to keep debating this, I was wrong, see above.

steep path
#

ok

hybrid karma
#

yeah, if at any point you reseed the local PRNG, that's it, that's the only input to the local PRNG 🙂

steep path
#

just wanted to make sure we don't have a bug

hybrid karma
#

np! happy to help clarify!

steep path
#

probably we should write a test for prng_reseed where we change the base seed and make sure the local results stay the same

hybrid karma
#

(I too am curious how you observed the thing you pasted earlier -- if there is a bug in the code or something)

deep night
#

My misunderstanding came from the SDK docs that state the two seeds are combined. I'll open a PR fixing that.

#

@hybrid karma Hmm, why do these two contracts with different base seeds always get the same values from the prng? 🤔


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

Is there something broken about as_contract's setup of a frame where the base seed isn't being used?

icy rock
#

So it is deterministic when you seed it?

deep night
#

as_contract uses the Host with_test_contract_frame fn.

icy rock
deep night
deep night
hybrid karma
#

hmm yes that does look .. wrong. I wonder what's happening there.

icy rock
#

Where is the implementation of set_base_brng_seed

hybrid karma
#

in the host. it's pretty straightforward.

#

(I'll look into this in a bit, my workspace is currently busy with something else)

icy rock
#

Ya so the state doesnt change is what youre saying

#

If calling set_base_prng_seed is supposed to affect the PRNG, but it doesn't itd seem set_base_prng_seed might not actually be affecting the ChaCha20Rng

hybrid karma
#

they're all ChaCha20Rngs

#

there's a base one that seeds frame local ones

#

changing the base one should change all the local ones seeded from it

#

it's not doing so

#

that's a bug

icy rock
#

Thanks making sure i understood

hybrid karma
#

(somewhere - might be env, might be sdk)

wild moth
#

So just to confirm if you set the seed the output will be deterministic across different contract calls

steep path
#

yes