#Is it expected that env.prng would generate two different values when doing a cross contract call?
104 messages · Page 1 of 1 (latest)
#![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)
}
}
sure
prng is independent between contract calls
you shouldn't be able to observe someone else's prng state
If I build and deploy that contract twice and call compare_prng the values will be different
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.
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
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?
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 😄
Hmm, actually if you provide your own seed it shouldn't be the same once deployed. In tests it will be because the "base" seed is always 0 in tests so that tests can be predictable (maybe that was a wrong call by me). But when deployed the base seed is always different for different contracts, and the seed you can set yourself is a sub-seed of the base seed.
Hmm..need to actually explore some but I'll be curious if when cross contract calling and asking for a prng if you could get the same value between two contracts when one is a sub call of the other
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
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
that's surprising. it's also not what I see in the test
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.
why not?
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.
right, but seeding should affect the current invocation, right?
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
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)
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.
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
Prng contains a pseudo-random number generator.
right, so it doesn't say anything specific about reseeding
and I think the behavior you have is incorrect or at least confusing
Contracts can only access (use or reseed) their local PRNG
^ word reseed is here
the whole point of reseeding is to get deterministic and predictable results in the the future
(or maybe I don't understand your example)
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.
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
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?
But is that what it does? The docs don't seem to say that, at least not as I read them.
the docs just say that the prng can be reseeded
The docs say only the local PRNG can be reseeded.
in any rng library I've dealt with that means that it will result in a well-defined sequence of generated numbers
sure
I'm not sure if that means the Local PRNG is affected by the base seed any longer.
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
I thought yes (base seed always affects) from conversations with @hybrid karma. But I might have misunderstood.
(I designed and wrote this code so I'm happy to answer questions!)
Thats the way seed works in the deploy function i actually woulda expected it here also.
there's a lot of questions going on here. I'm trying to untangle...
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?
no, it is not
Ok thanks, that's where my misunderstanding was about.
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
when you write "sub seed" what do you mean?
Did u guys read this link not just the docs it is explained in the code also...
So I think the behavior that @steep path was describing is correct.
Oops not the link but the link in that msg sorry
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);
});
}```
yes, that's what I'd expect to have happen. are you not observing it somewhere?
No, I was just wrong.
how are you observing this? what do you mean by "sub seed"?
(that is a question for @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
We don't need to keep debating this, I was wrong, see above.
ok
yeah, if at any point you reseed the local PRNG, that's it, that's the only input to the local PRNG 🙂
just wanted to make sure we don't have a bug
np! happy to help clarify!
probably we should write a test for prng_reseed where we change the base seed and make sure the local results stay the same
(I too am curious how you observed the thing you pasted earlier -- if there is a bug in the code or something)
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?
So it is deterministic when you seed it?
as_contract uses the Host with_test_contract_frame fn.
I thought the same thing from reviewing the code yesterday but i mostly read the comments which is probably where the docs are derived from
PR fixing incorrect docs: https://github.com/stellar/rs-soroban-sdk/pull/1128
Opened issue for this bug: https://github.com/stellar/rs-soroban-env/issues/1170
hmm yes that does look .. wrong. I wonder what's happening there.
Where is the implementation of set_base_brng_seed
in the host. it's pretty straightforward.
(I'll look into this in a bit, my workspace is currently busy with something else)
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
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
Thanks making sure i understood
(somewhere - might be env, might be sdk)
So just to confirm if you set the seed the output will be deterministic across different contract calls
yes