From d4a07fa83d234037d0eb173f9bc1502b3ae8d930 Mon Sep 17 00:00:00 2001 From: Alexander Skidanov Date: Mon, 24 Feb 2020 16:13:24 -0800 Subject: [PATCH] feat: Simple randomness beacon (#2177) feat: Simple randomness beacon This implements a simple biasable randomness beacon. The work on the unbiasable randomness beacon is here: https://github.com/nearprotocol/nearcore/commits/rnd_integration The biasable randomness beacon in this commit just uses the VRF created by the current block proposer, thus it has 1 bit of influence (the block proposer can choose not to create a block), which is sufficient for the block producers rotation, but should be used with caution for other use cases. Test plan --------- No new tests added as part of the change. --- chain/chain/src/chain.rs | 16 ++++++++++++ chain/chain/src/error.rs | 4 +++ chain/chain/src/test_utils.rs | 12 +++++++++ chain/chain/src/types.rs | 9 +++++++ core/crypto/src/lib.rs | 2 +- core/crypto/src/signature.rs | 14 ++++++++++ core/crypto/src/signer.rs | 12 +++++++++ core/crypto/src/traits.rs | 16 ++++++++++++ core/primitives/src/block.rs | 22 ++++++++++++++++ core/primitives/src/errors.rs | 5 ++++ core/primitives/src/validator_signer.rs | 19 ++++++++++++++ core/primitives/src/views.rs | 3 +++ genesis-tools/genesis-populate/src/lib.rs | 1 + near/src/runtime.rs | 32 ++++++++++++++++++++--- runtime/runtime/src/actions.rs | 22 ++++++++++++++++ test-utils/state-viewer/src/main.rs | 1 + 16 files changed, 186 insertions(+), 4 deletions(-) diff --git a/chain/chain/src/chain.rs b/chain/chain/src/chain.rs index 0d7e288b0d7..708b8d0ef2d 100644 --- a/chain/chain/src/chain.rs +++ b/chain/chain/src/chain.rs @@ -303,6 +303,7 @@ impl Chain { runtime_adapter.add_validator_proposals( CryptoHash::default(), genesis.hash(), + genesis.header.inner_rest.random_value, genesis.header.inner_lite.height, 0, vec![], @@ -709,6 +710,7 @@ impl Chain { self.runtime_adapter.add_validator_proposals( header.prev_hash, header.hash(), + header.inner_rest.random_value, header.inner_lite.height, self.store.get_block_height(&header.inner_rest.last_quorum_pre_commit)?, header.inner_rest.validator_proposals.clone(), @@ -2524,6 +2526,7 @@ impl<'a> ChainUpdate<'a> { let prev_prev_hash = prev.prev_hash; let prev_gas_price = prev.inner_rest.gas_price; let prev_epoch_id = prev.inner_lite.epoch_id.clone(); + let prev_random_value = prev.inner_rest.random_value; // Block is an orphan if we do not know about the previous full block. if !is_next && !self.chain_store_update.block_exists(&prev_hash)? { @@ -2559,6 +2562,18 @@ impl<'a> ChainUpdate<'a> { FinalityGadget::process_approval(me, approval, &mut self.chain_store_update)?; } + self.runtime_adapter.verify_block_vrf( + &block.header.inner_lite.epoch_id, + block.header.inner_lite.height, + &prev_random_value, + block.vrf_value, + block.vrf_proof, + )?; + + if block.header.inner_rest.random_value != hash(block.vrf_value.0.as_ref()) { + return Err(ErrorKind::InvalidRandomnessBeaconOutput.into()); + } + // We need to know the last approval on the previous block to later compute the reference // block for the current block. If it is not known by now, transfer it from the block // before it @@ -2635,6 +2650,7 @@ impl<'a> ChainUpdate<'a> { self.runtime_adapter.add_validator_proposals( block.header.prev_hash, block.hash(), + block.header.inner_rest.random_value, block.header.inner_lite.height, last_finalized_height, block.header.inner_rest.validator_proposals.clone(), diff --git a/chain/chain/src/error.rs b/chain/chain/src/error.rs index 8d0b1bce1a6..b104c305786 100644 --- a/chain/chain/src/error.rs +++ b/chain/chain/src/error.rs @@ -143,6 +143,9 @@ pub enum ErrorKind { /// Invalid shard id #[fail(display = "Invalid state request: {}", _0)] InvalidStateRequest(String), + /// Invalid VRF proof, or incorrect random_output in the header + #[fail(display = "Invalid Randomness Beacon Output")] + InvalidRandomnessBeaconOutput, /// Someone is not a validator. Usually happens in signature verification #[fail(display = "Not A Validator")] NotAValidator, @@ -250,6 +253,7 @@ impl Error { | ErrorKind::InvalidRent | ErrorKind::InvalidShardId(_) | ErrorKind::InvalidStateRequest(_) + | ErrorKind::InvalidRandomnessBeaconOutput | ErrorKind::NotAValidator => true, } } diff --git a/chain/chain/src/test_utils.rs b/chain/chain/src/test_utils.rs index 4a391436088..d909bcea3aa 100644 --- a/chain/chain/src/test_utils.rs +++ b/chain/chain/src/test_utils.rs @@ -265,6 +265,17 @@ impl RuntimeAdapter for KeyValueRuntime { Ok(()) } + fn verify_block_vrf( + &self, + _epoch_id: &EpochId, + _block_height: BlockHeight, + _prev_random_value: &CryptoHash, + _vrf_value: near_crypto::vrf::Value, + _vrf_proof: near_crypto::vrf::Proof, + ) -> Result<(), Error> { + Ok(()) + } + fn verify_validator_signature( &self, _epoch_id: &EpochId, @@ -454,6 +465,7 @@ impl RuntimeAdapter for KeyValueRuntime { &self, _parent_hash: CryptoHash, _current_hash: CryptoHash, + _rng_seed: CryptoHash, _height: BlockHeight, _last_finalized_height: BlockHeight, _proposals: Vec, diff --git a/chain/chain/src/types.rs b/chain/chain/src/types.rs index 62a812beac4..f0cc1efec81 100644 --- a/chain/chain/src/types.rs +++ b/chain/chain/src/types.rs @@ -118,6 +118,14 @@ pub trait RuntimeAdapter: Send + Sync { /// Verify block producer validity fn verify_block_signature(&self, header: &BlockHeader) -> Result<(), Error>; + fn verify_block_vrf( + &self, + epoch_id: &EpochId, + block_height: BlockHeight, + prev_random_value: &CryptoHash, + vrf_value: near_crypto::vrf::Value, + vrf_proof: near_crypto::vrf::Proof, + ) -> Result<(), Error>; /// Validates a given signed transaction on top of the given state root. /// Returns an option of `InvalidTxError`, it contains `Some(InvalidTxError)` if there is @@ -300,6 +308,7 @@ pub trait RuntimeAdapter: Send + Sync { &self, parent_hash: CryptoHash, current_hash: CryptoHash, + rng_seed: CryptoHash, height: BlockHeight, last_finalized_height: BlockHeight, proposals: Vec, diff --git a/core/crypto/src/lib.rs b/core/crypto/src/lib.rs index b5a2117a39c..c88684fe329 100644 --- a/core/crypto/src/lib.rs +++ b/core/crypto/src/lib.rs @@ -9,7 +9,7 @@ mod traits; #[macro_use] mod util; -mod key_conversion; +pub mod key_conversion; mod key_file; pub mod randomness; mod signature; diff --git a/core/crypto/src/signature.rs b/core/crypto/src/signature.rs index b1b04911ebb..4d2d2d43308 100644 --- a/core/crypto/src/signature.rs +++ b/core/crypto/src/signature.rs @@ -148,6 +148,13 @@ impl PublicKey { PublicKey::SECP256K1(_) => KeyType::SECP256K1, } } + + pub fn unwrap_as_ed25519(&self) -> &ED25519PublicKey { + match self { + PublicKey::ED25519(key) => &key, + PublicKey::SECP256K1(_) => panic!(), + } + } } impl Hash for PublicKey { @@ -373,6 +380,13 @@ impl SecretKey { } } } + + pub fn unwrap_as_ed25519(&self) -> &ED25519SecretKey { + match self { + SecretKey::ED25519(key) => &key, + SecretKey::SECP256K1(_) => panic!(), + } + } } impl std::fmt::Display for SecretKey { diff --git a/core/crypto/src/signer.rs b/core/crypto/src/signer.rs index 70950a0e537..60e9572c15a 100644 --- a/core/crypto/src/signer.rs +++ b/core/crypto/src/signer.rs @@ -1,6 +1,7 @@ use std::path::Path; use std::sync::Arc; +use crate::key_conversion::convert_secret_key; use crate::key_file::KeyFile; use crate::{KeyType, PublicKey, SecretKey, Signature}; @@ -13,6 +14,8 @@ pub trait Signer: Sync + Send { signature.verify(data, &self.public_key()) } + fn compute_vrf_with_proof(&self, _data: &[u8]) -> (crate::vrf::Value, crate::vrf::Proof); + /// Used by test infrastructure, only implement if make sense for testing otherwise raise `unimplemented`. fn write_to_file(&self, _path: &Path) { unimplemented!(); @@ -30,6 +33,10 @@ impl Signer for EmptySigner { fn sign(&self, _data: &[u8]) -> Signature { Signature::empty(KeyType::ED25519) } + + fn compute_vrf_with_proof(&self, _data: &[u8]) -> (crate::vrf::Value, crate::vrf::Proof) { + unimplemented!() + } } /// Signer that keeps secret key in memory. @@ -64,6 +71,11 @@ impl Signer for InMemorySigner { self.secret_key.sign(data) } + fn compute_vrf_with_proof(&self, data: &[u8]) -> (crate::vrf::Value, crate::vrf::Proof) { + let secret_key = convert_secret_key(self.secret_key.unwrap_as_ed25519()); + secret_key.compute_vrf_with_proof(&data) + } + fn write_to_file(&self, path: &Path) { KeyFile::from(self).write_to_file(path); } diff --git a/core/crypto/src/traits.rs b/core/crypto/src/traits.rs index b35debe6f3e..a8850c83bcb 100644 --- a/core/crypto/src/traits.rs +++ b/core/crypto/src/traits.rs @@ -150,6 +150,22 @@ macro_rules! value_type { } } + impl borsh::BorshSerialize for $ty { + #[inline] + fn serialize(&self, writer: &mut W) -> Result<(), std::io::Error> { + writer.write_all(&self.0) + } + } + + impl borsh::BorshDeserialize for $ty { + #[inline] + fn deserialize(reader: &mut R) -> Result { + let mut data = [0u8; $l]; + reader.read_exact(&mut data)?; + Ok($ty(data)) + } + } + common_conversions_fixed!($ty, $l, |s| &s.0, $what); }; } diff --git a/core/primitives/src/block.rs b/core/primitives/src/block.rs index b9f167a9ac4..4a3b7f181c6 100644 --- a/core/primitives/src/block.rs +++ b/core/primitives/src/block.rs @@ -46,6 +46,8 @@ pub struct BlockHeaderInnerRest { pub chunks_included: u64, /// Root hash of the challenges in the given block. pub challenges_root: MerkleHash, + /// The output of the randomness beacon + pub random_value: CryptoHash, /// Score. pub score: BlockScore, /// Validator proposals. @@ -107,6 +109,7 @@ impl BlockHeaderInnerRest { chunk_tx_root: MerkleHash, chunks_included: u64, challenges_root: MerkleHash, + random_value: CryptoHash, score: BlockScore, validator_proposals: Vec, chunk_mask: Vec, @@ -126,6 +129,7 @@ impl BlockHeaderInnerRest { chunk_tx_root, chunks_included, challenges_root, + random_value, score, validator_proposals, chunk_mask, @@ -261,6 +265,7 @@ impl BlockHeader { timestamp: u64, chunks_included: u64, challenges_root: MerkleHash, + random_value: CryptoHash, score: BlockScore, validator_proposals: Vec, chunk_mask: Vec, @@ -293,6 +298,7 @@ impl BlockHeader { chunk_tx_root, chunks_included, challenges_root, + random_value, score, validator_proposals, chunk_mask, @@ -337,6 +343,7 @@ impl BlockHeader { chunk_tx_root, chunks_included, challenges_root, + CryptoHash::default(), 0.into(), vec![], vec![], @@ -387,6 +394,10 @@ pub struct Block { pub header: BlockHeader, pub chunks: Vec, pub challenges: Challenges, + + // Data to confirm the correctness of randomness beacon output + pub vrf_value: near_crypto::vrf::Value, + pub vrf_proof: near_crypto::vrf::Proof, } pub fn genesis_chunks( @@ -449,6 +460,9 @@ impl Block { ), chunks, challenges, + + vrf_value: near_crypto::vrf::Value([0; 32]), + vrf_proof: near_crypto::vrf::Proof([0; 64]), } } @@ -509,6 +523,10 @@ impl Block { let time = if now <= prev.inner_lite.timestamp { prev.inner_lite.timestamp + 1 } else { now }; + let (vrf_value, vrf_proof) = + signer.compute_vrf_with_proof(prev.inner_rest.random_value.as_ref()); + let random_value = hash(vrf_value.0.as_ref()); + Block { header: BlockHeader::new( height, @@ -521,6 +539,7 @@ impl Block { time, Block::compute_chunks_included(&chunks, height), Block::compute_challenges_root(&challenges), + random_value, score, validator_proposals, chunk_mask, @@ -540,6 +559,9 @@ impl Block { ), chunks, challenges, + + vrf_value, + vrf_proof, } } diff --git a/core/primitives/src/errors.rs b/core/primitives/src/errors.rs index 65d607d597c..7674b694b0c 100644 --- a/core/primitives/src/errors.rs +++ b/core/primitives/src/errors.rs @@ -328,6 +328,8 @@ pub enum ActionErrorKind { #[serde(with = "u128_dec_format")] balance: Balance, }, + /// An attempt to stake with a key that is not convertable to ristretto + UnsuitableStakingKey { public_key: PublicKey }, /// An error occurred during a `FunctionCall` Action. FunctionCallError(FunctionCallError), /// Error occurs when a new `ActionReceipt` created by the `FunctionCall` action fails @@ -592,6 +594,9 @@ impl Display for ActionErrorKind { "Account {:?} tries to stake {}, but has staked {} and only has {}", account_id, stake, locked, balance ), + ActionErrorKind::UnsuitableStakingKey { public_key } => { + write!(f, "The staking key must be ED25519. {} is provided instead.", public_key) + } ActionErrorKind::CreateAccountNotAllowed { account_id, predecessor_id } => write!( f, "The new account_id {:?} can't be created by {:?}", diff --git a/core/primitives/src/validator_signer.rs b/core/primitives/src/validator_signer.rs index 35f2bed5d62..5b17eae15d3 100644 --- a/core/primitives/src/validator_signer.rs +++ b/core/primitives/src/validator_signer.rs @@ -58,6 +58,11 @@ pub trait ValidatorSigner: Sync + Send { epoch_id: &EpochId, ) -> Signature; + fn compute_vrf_with_proof( + &self, + data: &[u8], + ) -> (near_crypto::vrf::Value, near_crypto::vrf::Proof); + /// Used by test infrastructure, only implement if make sense for testing otherwise raise `unimplemented`. fn write_to_file(&self, path: &Path); } @@ -124,6 +129,13 @@ impl ValidatorSigner for EmptyValidatorSigner { Signature::default() } + fn compute_vrf_with_proof( + &self, + _data: &[u8], + ) -> (near_crypto::vrf::Value, near_crypto::vrf::Proof) { + unimplemented!() + } + fn write_to_file(&self, _path: &Path) { unimplemented!() } @@ -227,6 +239,13 @@ impl ValidatorSigner for InMemoryValidatorSigner { self.signer.sign(hash.as_ref()) } + fn compute_vrf_with_proof( + &self, + data: &[u8], + ) -> (near_crypto::vrf::Value, near_crypto::vrf::Proof) { + self.signer.compute_vrf_with_proof(data) + } + fn write_to_file(&self, path: &Path) { self.signer.write_to_file(path); } diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index c4e8c8a69b3..876d0d2b74c 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -275,6 +275,7 @@ pub struct BlockHeaderView { pub chunks_included: u64, pub challenges_root: CryptoHash, pub timestamp: u64, + pub random_value: CryptoHash, pub score: u64, pub validator_proposals: Vec, pub chunk_mask: Vec, @@ -311,6 +312,7 @@ impl From for BlockHeaderView { challenges_root: header.inner_rest.challenges_root, outcome_root: header.inner_lite.outcome_root, timestamp: header.inner_lite.timestamp, + random_value: header.inner_rest.random_value, score: header.inner_rest.score.to_num(), validator_proposals: header .inner_rest @@ -367,6 +369,7 @@ impl From for BlockHeader { chunk_tx_root: view.chunk_tx_root, chunks_included: view.chunks_included, challenges_root: view.challenges_root, + random_value: view.random_value, score: view.score.into(), validator_proposals: view .validator_proposals diff --git a/genesis-tools/genesis-populate/src/lib.rs b/genesis-tools/genesis-populate/src/lib.rs index a923e3a2a03..809ba8cd5ce 100644 --- a/genesis-tools/genesis-populate/src/lib.rs +++ b/genesis-tools/genesis-populate/src/lib.rs @@ -203,6 +203,7 @@ impl GenesisBuilder { .add_validator_proposals( CryptoHash::default(), genesis.hash(), + genesis.header.inner_rest.random_value, genesis.header.inner_lite.height, 0, vec![], diff --git a/near/src/runtime.rs b/near/src/runtime.rs index bee7831e648..d8a04f618e4 100644 --- a/near/src/runtime.rs +++ b/near/src/runtime.rs @@ -399,6 +399,27 @@ impl RuntimeAdapter for NightshadeRuntime { Ok(()) } + fn verify_block_vrf( + &self, + epoch_id: &EpochId, + block_height: BlockHeight, + prev_random_value: &CryptoHash, + vrf_value: near_crypto::vrf::Value, + vrf_proof: near_crypto::vrf::Proof, + ) -> Result<(), Error> { + let mut epoch_manager = self.epoch_manager.write().expect(POISONED_LOCK_ERR); + let validator = epoch_manager.get_block_producer_info(&epoch_id, block_height)?; + let public_key = near_crypto::key_conversion::convert_public_key( + validator.public_key.unwrap_as_ed25519(), + ) + .unwrap(); + + if !public_key.is_vrf_valid(&prev_random_value.as_ref(), &vrf_value, &vrf_proof) { + return Err(ErrorKind::InvalidRandomnessBeaconOutput.into()); + } + Ok(()) + } + fn validate_tx( &self, block_height: BlockHeight, @@ -784,6 +805,7 @@ impl RuntimeAdapter for NightshadeRuntime { &self, parent_hash: CryptoHash, current_hash: CryptoHash, + rng_seed: CryptoHash, height: BlockHeight, last_finalized_height: BlockHeight, proposals: Vec, @@ -809,8 +831,7 @@ impl RuntimeAdapter for NightshadeRuntime { validator_reward, total_supply, ); - // TODO: add randomness here - let rng_seed = [0; 32]; + let rng_seed = (rng_seed.0).0; // TODO: don't commit here, instead contribute to upstream store update. epoch_manager .record_block_info(¤t_hash, block_info, rng_seed)? @@ -1312,6 +1333,7 @@ mod test { .add_validator_proposals( CryptoHash::default(), genesis_hash, + [0; 32].as_ref().try_into().unwrap(), 0, 0, vec![], @@ -1379,6 +1401,7 @@ mod test { .add_validator_proposals( self.head.last_block_hash, new_hash, + [0; 32].as_ref().try_into().unwrap(), self.head.height + 1, self.head.height.saturating_sub(1), self.last_proposals.clone(), @@ -1501,8 +1524,10 @@ mod test { .unwrap() .iter() .map(|x| (x.0.account_id.clone(), x.1)) - .collect::>(), + .collect::>(), vec![("test3".to_string(), false), ("test1".to_string(), false)] + .into_iter() + .collect::>() ); let test1_acc = env.view_account("test1"); @@ -1785,6 +1810,7 @@ mod test { .add_validator_proposals( prev_hash, cur_hash, + [0; 32].as_ref().try_into().unwrap(), i, i.saturating_sub(2), new_env.last_proposals.clone(), diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index f482b92c08b..69a97e11443 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -26,6 +26,8 @@ use near_vm_logic::VMContext; use crate::config::{safe_add_gas, RuntimeConfig}; use crate::ext::RuntimeExt; use crate::{ActionResult, ApplyState}; +use near_crypto::key_conversion::convert_public_key; +use near_crypto::PublicKey; use near_primitives::errors::{ActionError, ActionErrorKind, RuntimeError}; use near_vm_errors::{CompilationError, FunctionCallError}; use near_vm_runner::VMError; @@ -230,6 +232,26 @@ pub(crate) fn action_stake( stake: &StakeAction, ) { let increment = stake.stake.saturating_sub(account.locked); + + // Make sure the key is ED25519, and can be converted to ristretto + match stake.public_key { + PublicKey::ED25519(key) => { + if convert_public_key(&key).is_none() { + result.result = Err(ActionErrorKind::UnsuitableStakingKey { + public_key: stake.public_key.clone(), + } + .into()); + return; + } + } + PublicKey::SECP256K1(_) => { + result.result = + Err(ActionErrorKind::UnsuitableStakingKey { public_key: stake.public_key.clone() } + .into()); + return; + } + }; + if account.amount >= increment { if account.locked == 0 && stake.stake == 0 { // if the account hasn't staked, it cannot unstake diff --git a/test-utils/state-viewer/src/main.rs b/test-utils/state-viewer/src/main.rs index d955b97990c..d9a1d3c294c 100644 --- a/test-utils/state-viewer/src/main.rs +++ b/test-utils/state-viewer/src/main.rs @@ -238,6 +238,7 @@ fn replay_chain( .add_validator_proposals( header.prev_hash, header.hash(), + header.inner_rest.random_value, header.inner_lite.height, chain_store .get_block_height(&header.inner_rest.last_quorum_pre_commit)