Skip to content

Commit

Permalink
Expose PRNG functions (#1023)
Browse files Browse the repository at this point in the history
### What

Expose `prng_reseed`, `u64_in_inclusive_range` and `vec_shuffle` in the
SDK.

### Why

Exposing the PRNG functions in the SDK allows users to utilize
randomness provided by the host in their smart contracts.

### Known limitations

N/A

Close #969

-----

I added tests for `sha256` and `verify_sig_ed25519` while I was in here.
Let me know if those changes should be moved to a separate PR. I
considered adding a PRNG module separate from the `crypto` module. I
decided to keep them under `crypto` because there are only three
functions. Also this is also similar `getRandomValues` is a function
implemented within the [Web Crypto
API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) in
browsers.

---------

Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com>
  • Loading branch information
masonforest and leighmcculloch authored Sep 16, 2023
1 parent 94f29fc commit 4efef11
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 2 deletions.
16 changes: 14 additions & 2 deletions soroban-sdk/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ use crate::auth::InvokerContractAuthEntry;
use crate::unwrap::UnwrapInfallible;
use crate::unwrap::UnwrapOptimized;
use crate::{
crypto::Crypto, deploy::Deployer, events::Events, ledger::Ledger, logs::Logs, storage::Storage,
Address, Vec,
crypto::Crypto, deploy::Deployer, events::Events, ledger::Ledger, logs::Logs, prng::Prng,
storage::Storage, Address, Vec,
};
use internal::{
AddressObject, Bool, BytesObject, DurationObject, I128Object, I256Object, I256Val, I64Object,
Expand Down Expand Up @@ -300,6 +300,17 @@ impl Env {
Crypto::new(self)
}

/// Get a [Prng] for accessing the current functions which provide pseudo-randomness.
///
/// # Warning
///
/// **The pseudo-random generator returned is not suitable for
/// security-sensitive work.**
#[inline(always)]
pub fn prng(&self) -> Prng {
Prng::new(self)
}

/// Get the Address object corresponding to the current executing contract.
pub fn current_contract_address(&self) -> Address {
let address = internal::Env::get_current_contract_address(self).unwrap_infallible();
Expand Down Expand Up @@ -518,6 +529,7 @@ impl Env {
env_impl
.set_diagnostic_level(internal::DiagnosticLevel::Debug)
.unwrap();
env_impl.set_base_prng_seed([0; 32]).unwrap();
let env = Env {
env_impl,
snapshot: None,
Expand Down
1 change: 1 addition & 0 deletions soroban-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,7 @@ pub mod iter;
pub mod ledger;
pub mod logs;
mod map;
pub mod prng;
pub mod storage;
pub mod token;
mod vec;
Expand Down
131 changes: 131 additions & 0 deletions soroban-sdk/src/prng.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//! Prng contains a pseudo-random generator.
//!
//! # Warning
//!
//! **The pseudo-random generator contained in this module is not suitable for
//! security-sensitive work.**
//!
//! The entropy used to seed the generator is not strong. Every node in the
//! network executing a contract get exactly the same output. The value is hard
//! to predict, but trivial to derive once the network has determined the inputs
//! into the ledger the invocation occurs in. The value is also controllable by
//! the node nominating. Therefore, the results of the pseudo-random number
//! generator are determinable once the inputs to a ledger are known.
//!
//! Every contract invocation gets its own, independent seed. If a contract
//! invocation fails, the seed from the failed invocation is not reused for the
//! next invocation of the contract.
//!
//! In tests, the contract invocation seed is consistently zero, and tests will
//! receive consistent results from the PRNG.
use core::ops::{Bound, RangeBounds};

use crate::{env::internal, unwrap::UnwrapInfallible, Bytes, Env, IntoVal, TryIntoVal, Val, Vec};

/// Prng is a pseudo-random generator.
///
/// # Warning
///
/// **The pseudo-random generator contained in this module is not suitable for
/// security-sensitive work.**
pub struct Prng {
env: Env,
}

impl Prng {
pub(crate) fn new(env: &Env) -> Prng {
Prng { env: env.clone() }
}

pub fn env(&self) -> &Env {
&self.env
}

/// Reseeds the PRNG with the provided value.
///
/// The seed is combined with the seed assigned to the contract invocation.
///
/// # Warning
///
/// **The pseudo-random generator contained in this module is not suitable for
/// security-sensitive work.**
pub fn seed(&self, seed: Bytes) {
let env = self.env();
internal::Env::prng_reseed(env, seed.into()).unwrap_infallible();
}

/// Returns a random u64 in the range specified.
///
/// # Panics
///
/// If the range is empty.
///
/// # Warning
///
/// **The pseudo-random generator contained in this module is not suitable for
/// security-sensitive work.**
///
/// # Examples
///
/// ```
/// use soroban_sdk::{Env};
///
/// # use soroban_sdk::{contract, contractimpl, symbol_short, Bytes};
/// #
/// # #[contract]
/// # pub struct Contract;
/// #
/// # #[cfg(feature = "testutils")]
/// # fn main() {
/// # let env = Env::default();
/// # let contract_id = env.register_contract(None, Contract);
/// # env.as_contract(&contract_id, || {
/// # env.prng().seed(Bytes::from_array(&env, &[1; 32]));
/// // Get values in the range of 1 to 100, inclusive.
/// let value = env.prng().u64_in_range(1..=100);
/// assert_eq!(value, 77);
/// let value = env.prng().u64_in_range(1..=100);
/// assert_eq!(value, 66);
/// let value = env.prng().u64_in_range(1..=100);
/// assert_eq!(value, 72);
/// # })
/// # }
/// # #[cfg(not(feature = "testutils"))]
/// # fn main() { }
/// ```
pub fn u64_in_range(&self, r: impl RangeBounds<u64>) -> u64 {
let start_bound = match r.start_bound() {
Bound::Included(b) => *b,
Bound::Excluded(b) => *b + 1,
Bound::Unbounded => 0,
};
let end_bound = match r.end_bound() {
Bound::Included(b) => *b,
Bound::Excluded(b) => *b - 1,
Bound::Unbounded => u64::MAX,
};
let env = self.env();
internal::Env::prng_u64_in_inclusive_range(env, start_bound.into(), end_bound.into())
.unwrap_infallible()
.into()
}

/// Shuffles a given vector v using the Fisher-Yates algorithm.
///
/// # Warning
///
/// **The pseudo-random generator contained in this module is not suitable for
/// security-sensitive work.**
pub fn shuffle<V>(&self, v: V) -> Vec<Val>
where
V: IntoVal<Env, Vec<Val>>,
{
let env = self.env();
let v_val = v.into_val(env);

internal::Env::prng_vec_shuffle(env, v_val.to_object())
.unwrap_infallible()
.try_into_val(env)
.unwrap_infallible()
}
}
3 changes: 3 additions & 0 deletions soroban-sdk/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ mod contract_udt_struct;
mod contract_udt_struct_tuple;
mod contractimport;
mod contractimport_with_error;
mod crypto_ed25519;
mod crypto_sha256;
mod env;
mod prng;
mod proptest_scval_cmp;
mod proptest_val_cmp;
mod token_client;
Expand Down
42 changes: 42 additions & 0 deletions soroban-sdk/src/tests/crypto_ed25519.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use crate::{bytes, bytesn, Env};

#[test]
fn test_verify_sig_ed25519() {
let env = Env::default();

// From https://datatracker.ietf.org/doc/html/rfc8032#section-7.1 TEST 2
let public_key = bytesn!(
&env,
0x3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c
);
let signature = bytesn!(
&env,
0x92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00
);
let message = bytes!(&env, 0x72);

env.crypto()
.ed25519_verify(&public_key, &message, &signature);
}

#[test]
#[should_panic(expected = "HostError: Error(Crypto, InvalidInput)")]
fn test_verify_sig_ed25519_invalid_sig() {
let env = Env::default();

// From https://datatracker.ietf.org/doc/html/rfc8032#section-7.1 TEST 2, message modified from 0x72 to 0x73
let public_key = bytesn!(
&env,
0x3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c
)
.try_into()
.unwrap();
let signature = bytesn!(
&env,
0x92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00
);
let message = bytes!(&env, 0x73);

env.crypto()
.ed25519_verify(&public_key, &message, &signature);
}
13 changes: 13 additions & 0 deletions soroban-sdk/src/tests/crypto_sha256.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use crate::{bytes, bytesn, Env};

#[test]
fn test_sha256() {
let env = Env::default();

let input = bytes!(&env, 0x01);
let expect = bytesn!(
&env,
0x4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a
);
assert_eq!(env.crypto().sha256(&input), expect);
}
87 changes: 87 additions & 0 deletions soroban-sdk/src/tests/prng.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use crate::{self as soroban_sdk};
use crate::{bytes, vec, Env, Val, Vec};
use soroban_sdk::contract;

#[contract]
pub struct TestPrngContract;

#[test]
fn test_prng_seed() {
let e = Env::default();
let id = e.register_contract(None, TestPrngContract);

e.as_contract(&id, || {
assert_eq!(e.prng().u64_in_range(0..=9), 6);
e.prng().seed(bytes!(
&e,
0x0000000000000000000000000000000000000000000000000000000000000001
));
assert_eq!(e.prng().u64_in_range(0..=9), 5);
});
}

#[test]
fn test_prng_shuffle() {
let e = Env::default();
let id = e.register_contract(None, TestPrngContract);

e.as_contract(&id, || {
let v = vec![&e, 1, 2, 3];
assert_eq!(e.prng().shuffle(v), vec![&e, 2, 3, 1].to_vals());
});

e.as_contract(&id, || {
let v = Vec::<i64>::new(&e);
assert_eq!(e.prng().shuffle(v), Vec::<Val>::new(&e).to_vals());
});
}

#[test]
fn test_vec_shuffle() {
let e = Env::default();
let id = e.register_contract(None, TestPrngContract);

e.as_contract(&id, || {
let v = vec![&e, 1, 2, 3];
let s = v.shuffle();
assert_eq!(s, vec![&e, 2, 3, 1]);
assert_eq!(v, vec![&e, 1, 2, 3]);
});

e.as_contract(&id, || {
let v = Vec::<i64>::new(&e);
let s = v.shuffle();
assert_eq!(s, vec![&e]);
assert_eq!(v, vec![&e]);
});
}

#[test]
fn test_prng_u64_in_range() {
let e = Env::default();
let id = e.register_contract(None, TestPrngContract);

e.as_contract(&id, || {
assert_eq!(e.prng().u64_in_range(..), 11654647981089815984);
assert_eq!(e.prng().u64_in_range(u64::MAX..), u64::MAX);
assert_eq!(
e.prng().u64_in_range(u64::MAX - 1..u64::MAX),
18446744073709551614
);
assert_eq!(e.prng().u64_in_range(u64::MAX..=u64::MAX), u64::MAX);
assert_eq!(e.prng().u64_in_range(0..1), 0);
assert_eq!(e.prng().u64_in_range(0..=0), 0);
assert_eq!(e.prng().u64_in_range(..=0), 0);
});
}

#[test]
#[should_panic(expected = "low > high")]
fn test_prng_u64_in_range_panic_on_empty_range() {
let e = Env::default();
let id = e.register_contract(None, TestPrngContract);

e.as_contract(&id, || {
e.prng().u64_in_range(u64::MAX..u64::MAX);
});
}
15 changes: 15 additions & 0 deletions soroban-sdk/src/vec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,21 @@ impl<T> Vec<T> {
unsafe { Self::unchecked_new(env.clone(), obj) }
}

/// Returns copy of the vec shuffled using the NOT-SECURE PRNG.
///
/// In tests, must be called from within a running contract.
///
/// # Warning
///
/// **The pseudo-random generator used to perform the shuffle is not
/// suitable for security-sensitive work.**
#[must_use]
pub fn shuffle(&self) -> Self {
let env = self.env();
let shuffled = env.prng().shuffle(self.to_vals());
unsafe { Self::unchecked_new(env.clone(), shuffled.obj) }
}

/// Returns true if the vec is empty and contains no items.
#[inline(always)]
pub fn is_empty(&self) -> bool {
Expand Down

0 comments on commit 4efef11

Please sign in to comment.