diff --git a/actors/evm/src/interpreter/precompiles/fvm.rs b/actors/evm/src/interpreter/precompiles/fvm.rs index dd9c52723..f44759262 100644 --- a/actors/evm/src/interpreter/precompiles/fvm.rs +++ b/actors/evm/src/interpreter/precompiles/fvm.rs @@ -220,3 +220,21 @@ pub(super) fn call_actor_shared( Ok(output) } + +/// Params: +/// +/// | Param | Value | +/// |------------------|---------------------------| +/// | epoch | U256 - low i64 | +/// +/// Errors if unable to fetch randomness +pub(super) fn get_randomness( + system: &mut System, + input: &[u8], + _: PrecompileContext, +) -> PrecompileResult { + let mut input_params = ValueReader::new(input); + let randomness_epoch = input_params.read_value()?; + let randomness = system.rt.get_beacon_randomness(randomness_epoch); + randomness.map(|r| r.to_vec()).map_err(|_| PrecompileError::InvalidInput) +} diff --git a/actors/evm/src/interpreter/precompiles/mod.rs b/actors/evm/src/interpreter/precompiles/mod.rs index 5faf7f68d..74625fc0f 100644 --- a/actors/evm/src/interpreter/precompiles/mod.rs +++ b/actors/evm/src/interpreter/precompiles/mod.rs @@ -13,7 +13,7 @@ mod evm; mod fvm; use evm::{blake2f, ec_add, ec_mul, ec_pairing, ec_recover, identity, modexp, ripemd160, sha256}; -use fvm::{call_actor, call_actor_id, lookup_delegated_address, resolve_address}; +use fvm::{call_actor, call_actor_id, get_randomness, lookup_delegated_address, resolve_address}; type PrecompileFn = fn(&mut System, &[u8], PrecompileContext) -> PrecompileResult; pub type PrecompileResult = Result, PrecompileError>; @@ -41,12 +41,13 @@ pub struct Precompiles(PhantomData); impl Precompiles { /// FEVM specific precompiles (0xfe prefix) - const NATIVE_PRECOMPILES: PrecompileTable = PrecompileTable([ + const NATIVE_PRECOMPILES: PrecompileTable = PrecompileTable([ Some(resolve_address::), // 0xfe00..01 Some(lookup_delegated_address::), // 0xfe00..02 Some(call_actor::), // 0xfe00..03 - None, // 0xfe00..04 DISABLED + None, // 0xfe00..04 get_actor_type DISABLED Some(call_actor_id::), // 0xfe00..05 + Some(get_randomness::), // 0xfe00..06 ]); /// EVM specific precompiles diff --git a/actors/evm/tests/precompile.rs b/actors/evm/tests/precompile.rs index 073e50940..f0915ae42 100644 --- a/actors/evm/tests/precompile.rs +++ b/actors/evm/tests/precompile.rs @@ -223,6 +223,44 @@ fn test_resolve_delegated() { rt.reset(); } +#[test] +fn test_precompile_randomness() { + let (init, body) = PrecompileTest::test_runner_assembly(); + let rt = + util::construct_and_verify(asm::new_contract("precompile-tester", &init, &body).unwrap()); + let rand_epoch = 100; + let rand_epoch_u256 = U256::from(rand_epoch); + { + // Underlying syscall succeeds. + let result = U256::from(0xdeadbeefu32).to_bytes(); + let test = PrecompileTest { + precompile_address: NativePrecompile::GetRandomness.eth_address(), + output_size: 32, + expected_exit_code: PrecompileExit::Success, + gas_avaliable: 10_000_000_000, + call_op: util::PrecompileCallOpcode::StaticCall, + input: rand_epoch_u256.to_bytes().to_vec(), + expected_return: result.to_vec(), + }; + rt.expect_get_beacon_randomness(rand_epoch, result, ExitCode::OK); + test.run_test(&rt); + } + { + // Underlying syscall fails. + let test = PrecompileTest { + precompile_address: NativePrecompile::GetRandomness.eth_address(), + output_size: 32, + expected_exit_code: PrecompileExit::Reverted, // Precompile reverts due to syscall failure + gas_avaliable: 10_000_000_000, + call_op: util::PrecompileCallOpcode::StaticCall, + input: rand_epoch_u256.to_bytes().to_vec(), + expected_return: vec![], + }; + rt.expect_get_beacon_randomness(rand_epoch, [0u8; 32], ExitCode::USR_ILLEGAL_ARGUMENT); + test.run_test(&rt); + } +} + #[test] fn test_precompile_transfer() { let (init, body) = util::PrecompileTest::test_runner_assembly(); diff --git a/actors/evm/tests/util.rs b/actors/evm/tests/util.rs index 335612fbb..b91ddc9f3 100644 --- a/actors/evm/tests/util.rs +++ b/actors/evm/tests/util.rs @@ -112,8 +112,9 @@ pub enum NativePrecompile { ResolveAddress = 1, LookupDelegatedAddress = 2, CallActor = 3, - GetActorType = 4, + GetActorTypeDISABLED = 4, CallActorId = 5, + GetRandomness = 6, } #[allow(dead_code)] diff --git a/runtime/src/runtime/fvm.rs b/runtime/src/runtime/fvm.rs index c87991dfb..810196b6c 100644 --- a/runtime/src/runtime/fvm.rs +++ b/runtime/src/runtime/fvm.rs @@ -234,8 +234,8 @@ where ) -> Result<[u8; RANDOMNESS_LENGTH], ActorError> { let digest = fvm::rand::get_chain_randomness(rand_epoch).map_err(|e| { match e { - ErrorNumber::LimitExceeded => { - actor_error!(illegal_argument; "randomness lookback exceeded: {}", e) + ErrorNumber::LimitExceeded | ErrorNumber::IllegalArgument => { + actor_error!(illegal_argument; "invalid lookback epoch: {}", e) } e => actor_error!(assertion_failed; "get chain randomness failed with an unexpected error: {}", e), } @@ -255,14 +255,7 @@ where rand_epoch: ChainEpoch, entropy: &[u8], ) -> Result<[u8; RANDOMNESS_LENGTH], ActorError> { - let digest = fvm::rand::get_beacon_randomness(rand_epoch).map_err(|e| { - match e { - ErrorNumber::LimitExceeded => { - actor_error!(illegal_argument; "randomness lookback exceeded: {}", e) - } - e => actor_error!(assertion_failed; "get beacon randomness failed with an unexpected error: {}", e), - } - })?; + let digest = self.get_beacon_randomness(rand_epoch)?; Ok(draw_randomness( fvm::crypto::hash_blake2b, &digest, @@ -272,6 +265,20 @@ where )) } + fn get_beacon_randomness( + &self, + rand_epoch: ChainEpoch, + ) -> Result<[u8; RANDOMNESS_LENGTH], ActorError> { + fvm::rand::get_beacon_randomness(rand_epoch).map_err(|e| { + match e { + ErrorNumber::LimitExceeded | ErrorNumber::IllegalArgument => { + actor_error!(illegal_argument; "invalid lookback epoch: {}", e) + } + e => actor_error!(assertion_failed; "get beacon randomness failed with an unexpected error: {}", e), + } + }) + } + fn get_state_root(&self) -> Result { Ok(fvm::sself::root()?) } diff --git a/runtime/src/runtime/mod.rs b/runtime/src/runtime/mod.rs index ec1e03b3e..5ce9d1424 100644 --- a/runtime/src/runtime/mod.rs +++ b/runtime/src/runtime/mod.rs @@ -117,6 +117,14 @@ pub trait Runtime: Primitives + RuntimePolicy { entropy: &[u8], ) -> Result<[u8; RANDOMNESS_LENGTH], ActorError>; + /// Returns a (pseudo)random byte array drawing from the latest + /// beacon from a given epoch. + /// This randomness is not tied to any fork of the chain, and is unbiasable. + fn get_beacon_randomness( + &self, + rand_epoch: ChainEpoch, + ) -> Result<[u8; RANDOMNESS_LENGTH], ActorError>; + /// Initializes the state object. /// This is only valid when the state has not yet been initialized. /// NOTE: we should also limit this to being invoked during the constructor method diff --git a/runtime/src/test_utils.rs b/runtime/src/test_utils.rs index 4c10379ce..643b245a4 100644 --- a/runtime/src/test_utils.rs +++ b/runtime/src/test_utils.rs @@ -200,6 +200,7 @@ pub struct Expectations { pub expect_verify_consensus_fault: Option, pub expect_get_randomness_tickets: VecDeque, pub expect_get_randomness_beacon: VecDeque, + pub expect_get_beacon_randomness: VecDeque, pub expect_batch_verify_seals: Option, pub expect_aggregate_verify_seals: Option, pub expect_replica_verify: VecDeque, @@ -285,6 +286,11 @@ impl Expectations { "expect_get_randomness_beacon {:?}, not received", this.expect_get_randomness_beacon ); + assert!( + this.expect_get_beacon_randomness.is_empty(), + "expect_get_beacon_randomness {:?}, not received", + this.expect_get_beacon_randomness + ); assert!( this.expect_batch_verify_seals.is_none(), "expect_batch_verify_seals {:?}, not received", @@ -422,6 +428,13 @@ pub struct ExpectRandomness { out: [u8; RANDOMNESS_LENGTH], } +#[derive(Clone, Debug)] +pub struct ExpectGetBeacon { + epoch: ChainEpoch, + out: [u8; RANDOMNESS_LENGTH], + exit_code: ExitCode, +} + #[derive(Debug)] pub struct ExpectBatchVerifySeals { input: Vec, @@ -748,6 +761,17 @@ impl MockRuntime { self.expectations.borrow_mut().expect_get_randomness_beacon.push_back(a); } + #[allow(dead_code)] + pub fn expect_get_beacon_randomness( + &self, + epoch: ChainEpoch, + out: [u8; RANDOMNESS_LENGTH], + exit_code: ExitCode, + ) { + let a = ExpectGetBeacon { epoch, out, exit_code }; + self.expectations.borrow_mut().expect_get_beacon_randomness.push_back(a); + } + #[allow(dead_code)] pub fn expect_batch_verify_seals( &self, @@ -1017,7 +1041,6 @@ impl Runtime for MockRuntime { .pop_front() .expect("unexpected call to get_randomness_from_tickets"); - assert!(epoch <= *self.epoch.borrow(), "attempt to get randomness from future"); assert_eq!( expected.tag, tag, "unexpected domain separation tag, expected: {:?}, actual: {:?}", @@ -1050,7 +1073,6 @@ impl Runtime for MockRuntime { .pop_front() .expect("unexpected call to get_randomness_from_beacon"); - assert!(epoch <= *self.epoch.borrow(), "attempt to get randomness from future"); assert_eq!( expected.tag, tag, "unexpected domain separation tag, expected: {:?}, actual: {:?}", @@ -1070,6 +1092,28 @@ impl Runtime for MockRuntime { Ok(expected.out) } + fn get_beacon_randomness( + &self, + epoch: ChainEpoch, + ) -> Result<[u8; RANDOMNESS_LENGTH], ActorError> { + let exp = self + .expectations + .borrow_mut() + .expect_get_beacon_randomness + .pop_front() + .expect("unexpected call to get_randomness_from_beacon"); + + assert_eq!( + exp.epoch, epoch, + "unexpected epoch, expected: {:?}, actual: {:?}", + exp.epoch, epoch + ); + if exp.exit_code != ExitCode::OK { + return Err(ActorError::unchecked(exp.exit_code, "Expected Failure".to_string())); + } + Ok(exp.out) + } + fn create(&self, obj: &T) -> Result<(), ActorError> { if self.state.borrow().is_some() { return Err(actor_error!(illegal_state; "state already constructed")); diff --git a/test_vm/src/messaging.rs b/test_vm/src/messaging.rs index 70521f9c3..2257bad83 100644 --- a/test_vm/src/messaging.rs +++ b/test_vm/src/messaging.rs @@ -549,6 +549,13 @@ impl<'invocation> Runtime for InvocationCtx<'invocation> { Ok(TEST_VM_RAND_ARRAY) } + fn get_beacon_randomness( + &self, + _rand_epoch: ChainEpoch, + ) -> Result<[u8; RANDOMNESS_LENGTH], ActorError> { + Ok(TEST_VM_RAND_ARRAY) + } + fn get_state_root(&self) -> Result { Ok(self.v.actor(&self.to()).unwrap().state) }