From 565c31e6c58206d428c2585b25762293a221d335 Mon Sep 17 00:00:00 2001 From: Andreas Fackler Date: Fri, 26 Jul 2019 09:27:37 +0200 Subject: [PATCH] Add randomness contract support to Authority Round. Changes have been cherry-picked from poanetwork's aura-pos branch. Most of the work has been done by @mbr. --- Cargo.lock | 5 + ethcore/Cargo.toml | 3 +- ethcore/client-traits/src/lib.rs | 24 +- ethcore/engine/src/engine.rs | 8 +- ethcore/engine/src/signer.rs | 15 + ethcore/engine/src/test_helpers.rs | 11 + ethcore/engines/authority-round/Cargo.toml | 5 + ethcore/engines/authority-round/src/lib.rs | 126 ++++++++- .../engines/authority-round/src/randomness.rs | 245 ++++++++++++++++ ethcore/engines/authority-round/src/util.rs | 78 ++++++ ethcore/engines/validator-set/src/multi.rs | 4 +- .../validator-set/src/safe_contract.rs | 4 +- .../authority_round_randomness_contract.json | 60 ++++ .../res/contracts/authority_round_random.json | 158 +++++++++++ .../test_authority_round_random.json | 265 ++++++++++++++++++ .../contracts/test_authority_round_random.sol | 107 +++++++ ethcore/spec/src/chain.rs | 1 + ethcore/src/client/client.rs | 27 +- ethcore/src/client/test_client.rs | 31 +- ethcore/src/lib.rs | 4 +- ethcore/src/miner/miner.rs | 48 +++- ethcore/src/snapshot/service.rs | 2 +- ethcore/src/snapshot/tests/service.rs | 8 +- ethcore/src/test_helpers.rs | 14 +- ethcore/types/src/errors/engine_error.rs | 12 + json/src/spec/authority_round.rs | 25 +- rpc/src/v1/helpers/engine_signer.rs | 11 + 27 files changed, 1246 insertions(+), 55 deletions(-) create mode 100644 ethcore/engines/authority-round/src/randomness.rs create mode 100644 ethcore/engines/authority-round/src/util.rs create mode 100644 ethcore/res/authority_round_randomness_contract.json create mode 100644 ethcore/res/contracts/authority_round_random.json create mode 100644 ethcore/res/contracts/test_authority_round_random.json create mode 100644 ethcore/res/contracts/test_authority_round_random.sol diff --git a/Cargo.lock b/Cargo.lock index d4867ad3362..bc1935e0910 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,8 +168,12 @@ dependencies = [ "block-reward 0.1.0", "client-traits 0.1.0", "common-types 0.1.0", + "derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "engine 0.1.0", "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "ethabi 8.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ethabi-contract 8.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ethabi-derive 8.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "ethcore 1.12.0", "ethcore-accounts 0.1.0", "ethcore-io 1.12.0", @@ -183,6 +187,7 @@ dependencies = [ "lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "machine 0.1.0", "macros 0.1.0", + "parity-bytes 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "rlp 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/ethcore/Cargo.toml b/ethcore/Cargo.toml index d87133a502b..6cef603e651 100644 --- a/ethcore/Cargo.toml +++ b/ethcore/Cargo.toml @@ -83,7 +83,6 @@ ethcore-builtin = { path = "./builtin" } criterion = "0.2" engine = { path = "./engine", features = ["test-helpers"] } env_logger = "0.5" -ethash = { path = "../ethash" } ethcore-accounts = { path = "../accounts" } ethjson = { path = "../json" } ethkey = { path = "../accounts/ethkey" } @@ -105,7 +104,7 @@ parity = ["work-notify", "price-info", "stratum", "macros"] # but might be omitted for other dependent crates. work-notify = ["ethcore-miner/work-notify"] price-info = ["ethcore-miner/price-info"] -stratum = ["ethcore-stratum"] +stratum = ["ethash", "ethcore-stratum"] # Disables seal verification for mined blocks. # This allows you to submit any seal via RPC to test and benchmark diff --git a/ethcore/client-traits/src/lib.rs b/ethcore/client-traits/src/lib.rs index af68b9b3f6c..aa15b4aaaf1 100644 --- a/ethcore/client-traits/src/lib.rs +++ b/ethcore/client-traits/src/lib.rs @@ -40,7 +40,7 @@ use common_types::{ pruning_info::PruningInfo, receipt::LocalizedReceipt, trace_filter::Filter as TraceFilter, - transaction::{self, LocalizedTransaction, CallError, SignedTransaction}, + transaction::{self, Action, LocalizedTransaction, CallError, SignedTransaction}, tree_route::TreeRoute, verification::{VerificationQueueInfo, Unverified}, }; @@ -367,7 +367,27 @@ pub trait BlockChainClient : Sync + Send + AccountData + BlockChain + CallContra fn pruning_info(&self) -> PruningInfo; /// Schedule state-altering transaction to be executed on the next pending block. - fn transact_contract(&self, address: Address, data: Bytes) -> Result<(), transaction::Error>; + fn transact_contract(&self, address: Address, data: Bytes) -> Result<(), transaction::Error> { + self.transact(Action::Call(address), data, None, None, None) + } + + /// Returns a signed transaction. If gas limit, gas price, or nonce are not + /// specified, the defaults are used. + fn create_transaction( + &self, + action: Action, + data: Bytes, + gas: Option, + gas_price: Option, + nonce: Option + ) -> Result; + + /// Schedule state-altering transaction to be executed on the next pending + /// block with the given gas and nonce parameters. + /// + /// If they are `None`, sensible values are selected automatically. + fn transact(&self, action: Action, data: Bytes, gas: Option, gas_price: Option, nonce: Option) + -> Result<(), transaction::Error>; /// Get the address of the registry itself. fn registrar_address(&self) -> Option
; diff --git a/ethcore/engine/src/engine.rs b/ethcore/engine/src/engine.rs index 815c14b3896..e614266e425 100644 --- a/ethcore/engine/src/engine.rs +++ b/ethcore/engine/src/engine.rs @@ -32,7 +32,7 @@ use common_types::{ }, errors::{EthcoreError as Error, EngineError}, snapshot::Snapshotting, - transaction::{self, UnverifiedTransaction}, + transaction::{self, SignedTransaction, UnverifiedTransaction}, }; use client_traits::EngineClient; @@ -185,6 +185,12 @@ pub trait Engine: Sync + Send { /// Allow mutating the header during seal generation. Currently only used by Clique. fn on_seal_block(&self, _block: &mut ExecutedBlock) -> Result<(), Error> { Ok(()) } + /// Returns a list of transactions for a new block if we are the author. + /// + /// This is called when the miner prepares a new block that this node will author and seal. It returns a list of + /// transactions that will be added to the block before any other transactions from the queue. + fn on_prepare_block(&self, _block: &ExecutedBlock) -> Result, Error> { Ok(Vec::new()) } + /// Returns the engine's current sealing state. fn sealing_state(&self) -> SealingState { SealingState::External } diff --git a/ethcore/engine/src/signer.rs b/ethcore/engine/src/signer.rs index 1e932feefc3..25641e47c9f 100644 --- a/ethcore/engine/src/signer.rs +++ b/ethcore/engine/src/signer.rs @@ -18,6 +18,7 @@ use ethereum_types::{H256, Address}; use ethkey::{self, Signature}; +use ethkey::crypto::ecies; /// Everything that an Engine needs to sign messages. pub trait EngineSigner: Send + Sync { @@ -26,6 +27,12 @@ pub trait EngineSigner: Send + Sync { /// Signing address fn address(&self) -> Address; + + /// Decrypt a message that was encrypted to this signer's key. + fn decrypt(&self, auth_data: &[u8], cipher: &[u8]) -> Result, ethkey::crypto::Error>; + + /// The signer's public key, if available. + fn public(&self) -> Option; } /// Creates a new `EngineSigner` from given key pair. @@ -40,7 +47,15 @@ impl EngineSigner for Signer { ethkey::sign(self.0.secret(), &hash) } + fn decrypt(&self, auth_data: &[u8], cipher: &[u8]) -> Result, ethkey::crypto::Error> { + ecies::decrypt(self.0.secret(), auth_data, cipher) + } + fn address(&self) -> Address { self.0.address() } + + fn public(&self) -> Option { + Some(*self.0.public()) + } } diff --git a/ethcore/engine/src/test_helpers.rs b/ethcore/engine/src/test_helpers.rs index 92ba66ff480..6bc322d7834 100644 --- a/ethcore/engine/src/test_helpers.rs +++ b/ethcore/engine/src/test_helpers.rs @@ -43,7 +43,18 @@ impl EngineSigner for (Arc, Address, Password) { } } + fn decrypt(&self, auth_data: &[u8], cipher: &[u8]) -> Result, ethkey::crypto::Error> { + self.0.decrypt(self.1, None, auth_data, cipher).map_err(|e| { + warn!("Unable to decrypt message: {:?}", e); + ethkey::crypto::Error::InvalidMessage + }) + } + fn address(&self) -> Address { self.1 } + + fn public(&self) -> Option { + self.0.account_public(self.1, &self.2).ok() + } } diff --git a/ethcore/engines/authority-round/Cargo.toml b/ethcore/engines/authority-round/Cargo.toml index bd4879ac67e..ec90c6442b8 100644 --- a/ethcore/engines/authority-round/Cargo.toml +++ b/ethcore/engines/authority-round/Cargo.toml @@ -10,6 +10,10 @@ license = "GPL-3.0" block-reward = { path = "../../block-reward" } client-traits = { path = "../../client-traits" } common-types = { path = "../../types" } +derive_more = "0.15.0" +ethabi = "8.0" +ethabi-contract = "8.0" +ethabi-derive = "8.0" ethereum-types = "0.6.0" ethjson = { path = "../../../json" } ethkey = { path = "../../../accounts/ethkey" } @@ -22,6 +26,7 @@ log = "0.4" lru-cache = "0.1" machine = { path = "../../machine" } macros = { path = "../../../util/macros" } +parity-bytes = "0.1" parking_lot = "0.8" rand = "0.6" rlp = "0.4.0" diff --git a/ethcore/engines/authority-round/src/lib.rs b/ethcore/engines/authority-round/src/lib.rs index 98aa8c22add..13f9ff99c7b 100644 --- a/ethcore/engines/authority-round/src/lib.rs +++ b/ethcore/engines/authority-round/src/lib.rs @@ -22,7 +22,7 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::{cmp, fmt}; -use std::iter::FromIterator; +use std::iter::{self, FromIterator}; use std::ops::Deref; use std::sync::atomic::{AtomicUsize, AtomicBool, Ordering as AtomicOrdering}; use std::sync::{Weak, Arc}; @@ -43,9 +43,11 @@ use engine::signer::EngineSigner; use ethkey::{self, Signature}; use io::{IoContext, IoHandler, TimerToken, IoService}; use itertools::{self, Itertools}; +use rand::rngs::OsRng; use rlp::{encode, Decodable, DecoderError, Encodable, RlpStream, Rlp}; use ethereum_types::{H256, H520, Address, U128, U256}; use parking_lot::{Mutex, RwLock}; +use parity_bytes::Bytes; use time_utils::CheckedSystemTime; use common_types::{ ancestry_action::AncestryAction, @@ -60,13 +62,17 @@ use common_types::{ machine::{Call, AuxiliaryData}, }, errors::{BlockError, EthcoreError as Error, EngineError}, + ids::BlockId, snapshot::Snapshotting, + transaction::{Action, SignedTransaction}, }; use unexpected::{Mismatch, OutOfBounds}; use validator_set::{ValidatorSet, SimpleList, new_validator_set}; mod finality; +mod randomness; +pub(crate) mod util; use self::finality::RollingFinality; @@ -104,6 +110,8 @@ pub struct AuthorityRoundParams { pub maximum_empty_steps: usize, /// Transition block to strict empty steps validation. pub strict_empty_steps_transition: u64, + /// If set, enables random number contract integration. It maps the transition block to the contract address. + pub randomness_contract_address: BTreeMap, } const U16_MAX: usize = ::std::u16::MAX as usize; @@ -139,6 +147,17 @@ impl From for AuthorityRoundParams { BlockRewardContract::new_from_address(address.into()) ); } + let randomness_contract_address = match p.randomness_contract_address { + None => BTreeMap::new(), + Some(ethjson::spec::authority_round::TransitionMap::Single(addr)) => { + iter::once((0, addr.into())).collect() + } + Some(ethjson::spec::authority_round::TransitionMap::Transitions(transitions)) => { + transitions.into_iter().map(|(ethjson::uint::Uint(block), addr)| { + (block.as_u64(), addr.into()) + }).collect() + } + }; AuthorityRoundParams { step_duration: step_duration_usize as u16, validators: new_validator_set(p.validators), @@ -154,6 +173,7 @@ impl From for AuthorityRoundParams { maximum_empty_steps: p.maximum_empty_steps.map_or(0, Into::into), two_thirds_majority_transition: p.two_thirds_majority_transition.map_or_else(BlockNumber::max_value, Into::into), strict_empty_steps_transition: p.strict_empty_steps_transition.map_or(0, Into::into), + randomness_contract_address, } } } @@ -482,6 +502,8 @@ pub struct AuthorityRound { two_thirds_majority_transition: BlockNumber, maximum_empty_steps: usize, machine: Machine, + /// If set, enables random number contract integration. It maps the transition block to the contract address. + randomness_contract_address: BTreeMap, } // header-chain validator. @@ -751,6 +773,7 @@ impl AuthorityRound { two_thirds_majority_transition: our_params.two_thirds_majority_transition, strict_empty_steps_transition: our_params.strict_empty_steps_transition, machine, + randomness_contract_address: our_params.randomness_contract_address, }); // Do not initialize timeouts for tests. @@ -1224,7 +1247,6 @@ impl Engine for AuthorityRound { // report any skipped primaries between the parent block and // the block we're sealing, unless we have empty steps enabled if header.number() < self.empty_steps_transition { - trace!(target: "engine", "generate_seal: reporting misbehaviour for step={}, block=#{}", step, header.number()); self.report_skipped(header, step, parent_step, &*validators, epoch_transition_number); } @@ -1328,6 +1350,48 @@ impl Engine for AuthorityRound { block_reward::apply_block_rewards(&rewards, block, &self.machine) } + /// Make calls to the randomness and validator set contracts. + fn on_prepare_block(&self, block: &ExecutedBlock) -> Result, Error> { + let opt_signer = self.signer.read(); + let signer = match opt_signer.as_ref() { + Some(signer) => signer, + None => return Ok(Vec::new()), // We are not a validator, so we shouldn't call the contracts. + }; + let our_addr = signer.address(); + let client = self.client.read().as_ref().and_then(|weak| weak.upgrade()).ok_or_else(|| { + debug!(target: "engine", "Unable to prepare block: missing client ref."); + EngineError::RequiresClient + })?; + let full_client = client.as_full_client() + .ok_or(EngineError::FailedSystemCall("Failed to upgrade to BlockchainClient.".to_string()))?; + + // Our current account nonce. The transactions must have consecutive nonces, starting with this one. + let mut tx_nonce = block.state.nonce(&our_addr)?; + let mut transactions = Vec::new(); + + // Creates and signs a transaction with the given contract call. + let mut make_transaction = |to: Address, data: Bytes| -> Result { + let nonce = Some(tx_nonce); + tx_nonce += U256::one(); // Increment the nonce for the next transaction. + Ok(full_client.create_transaction(Action::Call(to), data, None, Some(U256::zero()), nonce)?) + }; + + // Random number generation + if let Some((_, &contract_addr)) = self.randomness_contract_address.range(..=block.header.number()).last() { + let contract = util::BoundContract::bind(&*client, BlockId::Latest, contract_addr); + let phase = randomness::RandomnessPhase::load(&contract, our_addr) + .map_err(|err| EngineError::RandomnessLoadError(err.to_string()))?; + // TODO: With rand 0.7, `OsRng` can be used directly. + let mut rng = OsRng::new().map_err(|err| err.to_string())?; + if let Some(data) = phase.advance(&contract, &mut rng, signer.as_ref()) + .map_err(|err| EngineError::RandomnessAdvanceError(err.to_string()))? { + transactions.push(make_transaction(contract_addr, data)?); + } + } + + Ok(transactions) + } + /// Check the number of seal fields. fn verify_block_basic(&self, header: &Header) -> Result<(), Error> { if header.number() >= self.validate_score_transition && *header.difficulty() >= U256::from(U128::max_value()) { @@ -1668,19 +1732,22 @@ mod tests { use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering}; use keccak_hash::keccak; use accounts::AccountProvider; + use ethabi_contract::use_contract; use ethereum_types::{Address, H520, H256, U256}; use ethkey::Signature; use common_types::{ header::Header, engines::{Seal, params::CommonParams}, + ids::BlockId, errors::{EthcoreError as Error, EngineError}, transaction::{Action, Transaction}, }; use rlp::encode; use ethcore::{ block::*, + miner::{Author, MinerService}, test_helpers::{ - generate_dummy_client_with_spec, get_temp_state_db, + generate_dummy_client_with_spec, generate_dummy_client_with_spec_and_data, get_temp_state_db, TestNotify }, }; @@ -1692,7 +1759,7 @@ mod tests { use ethjson; use serde_json; - use super::{AuthorityRoundParams, AuthorityRound, EmptyStep, SealedEmptyStep, calculate_score}; + use super::{AuthorityRoundParams, AuthorityRound, EmptyStep, SealedEmptyStep, calculate_score, util::BoundContract}; fn build_aura(f: F) -> Arc where F: FnOnce(&mut AuthorityRoundParams), @@ -1712,6 +1779,7 @@ mod tests { block_reward_contract_transitions: Default::default(), strict_empty_steps_transition: 0, two_thirds_majority_transition: 0, + randomness_contract_address: BTreeMap::new(), }; // mutate aura params @@ -1895,8 +1963,6 @@ mod tests { #[test] fn reports_skipped() { - let _ = ::env_logger::try_init(); - let validator1 = Address::from_low_u64_be(1); let validator2 = Address::from_low_u64_be(2); let last_benign = Arc::new(AtomicUsize::new(0)); @@ -2329,6 +2395,54 @@ mod tests { ) } + #[test] + fn randomness_contract() -> Result<(), super::util::CallError> { + use_contract!(rand_contract, "../../res/contracts/test_authority_round_random.json"); + + env_logger::init(); + + let contract_addr = Address::from_str("0000000000000000000000000000000000000042").unwrap(); + let client = generate_dummy_client_with_spec_and_data( + spec::new_test_round_randomness_contract, 0, 0, &[], true + ); + + let tap = Arc::new(AccountProvider::transient_provider()); + + let addr1 = tap.insert_account(keccak("1").into(), &"1".into()).unwrap(); + // Unlock account so that the engine can decrypt the secret. + tap.unlock_account_permanently(addr1, "1".into()).expect("unlock"); + + let signer = Box::new((tap.clone(), addr1, "1".into())); + client.miner().set_author(Author::Sealer(signer.clone())); + client.miner().set_gas_range_target((U256::from(1000000), U256::from(1000000))); + + let engine = client.engine(); + engine.set_signer(signer); + engine.register_client(Arc::downgrade(&client) as _); + let bc = BoundContract::bind(&*client, BlockId::Latest, contract_addr); + + // First the contract is in the commit phase, and we haven't committed yet. + assert!(bc.call_const(rand_contract::functions::is_commit_phase::call())?); + assert!(!bc.call_const(rand_contract::functions::is_committed::call(0, addr1))?); + + // We produce a block and commit. + engine.step(); + assert!(bc.call_const(rand_contract::functions::is_committed::call(0, addr1))?); + + // After two more blocks we are in the reveal phase... + engine.step(); + engine.step(); + assert!(bc.call_const(rand_contract::functions::is_reveal_phase::call())?); + assert!(!bc.call_const(rand_contract::functions::sent_reveal::call(0, addr1))?); + assert!(bc.call_const(rand_contract::functions::get_value::call())?.is_zero()); + + // ...so in the next step, we reveal our random value, and the contract's random value is not zero anymore. + engine.step(); + assert!(bc.call_const(rand_contract::functions::sent_reveal::call(0, addr1))?); + assert!(!bc.call_const(rand_contract::functions::get_value::call())?.is_zero()); + Ok(()) + } + #[test] fn extra_info_from_seal() { let (spec, tap, accounts) = setup_empty_steps(); diff --git a/ethcore/engines/authority-round/src/randomness.rs b/ethcore/engines/authority-round/src/randomness.rs new file mode 100644 index 00000000000..3abd5460cc7 --- /dev/null +++ b/ethcore/engines/authority-round/src/randomness.rs @@ -0,0 +1,245 @@ +//! On-chain randomness generation for authority round +//! +//! This module contains the support code for the on-chain randomness generation used by AuRa. Its +//! core is the finite state machine `RandomnessPhase`, which can be loaded from the blockchain +//! state, then asked to perform potentially necessary transaction afterwards using the `advance()` +//! method. +//! +//! No additional state is kept inside the `RandomnessPhase`, it must be passed in each time. + +use derive_more::Display; +use ethabi::Hash; +use ethabi_contract::use_contract; +use ethereum_types::{Address, U256}; +use ethkey::crypto::ecies; +use keccak_hash::keccak; +use log::{error, trace}; +use parity_bytes::Bytes; +use rand::Rng; +use engine::signer::EngineSigner; + +use crate::util::{BoundContract, CallError}; + +/// Secret type expected by the contract. +// Note: Conversion from `U256` back into `[u8; 32]` is cumbersome (missing implementations), for +// this reason we store the raw buffers. +pub type Secret = [u8; 32]; + +use_contract!(aura_random, "../../res/contracts/authority_round_random.json"); + +/// Validated randomness phase state. +/// +/// The process of generating random numbers is a simple finite state machine: +/// +/// ```text +/// + +/// | +/// | +/// | +/// +--------------+ +-------v-------+ +/// | | | | +/// | BeforeCommit <------------------------------+ Waiting | +/// | | enter commit phase | | +/// +------+-------+ +-------^-------+ +/// | | +/// | call | +/// | `commitHash()` | call +/// | | `revealSecret` +/// | | +/// +------v-------+ +-------+-------+ +/// | | | | +/// | Committed +------------------------------> Reveal | +/// | | enter reveal phase | | +/// +--------------+ +---------------+ +/// ``` +/// +/// +/// Phase transitions are performed by the smart contract and simply queried by the engine. +/// +/// A typical case of using `RandomnessPhase` is: +/// +/// 1. `RandomnessPhase::load()` the phase from the blockchain data. +/// 2. Call `RandomnessPhase::advance()`. +#[derive(Debug)] +pub enum RandomnessPhase { + // NOTE: Some states include information already gathered during `load` (e.g. `our_address`, + // `round`) for efficiency reasons. + /// Waiting for the next phase. + /// + /// This state indicates either the successful revelation in this round or having missed the + /// window to make a commitment. + Waiting, + /// Indicates a commitment is possible, but still missing. + BeforeCommit { our_address: Address, round: U256 }, + /// Indicates a successful commitment, waiting for the commit phase to end. + Committed, + /// Indicates revealing is expected as the next step. + Reveal { our_address: Address, round: U256 }, +} + +/// Phase loading error for randomness generation state machine. +/// +/// This error usually indicates a bug in either the smart contract, the phase loading function or +/// some state being lost. +/// +/// The `LostSecret` and `StaleSecret` will usually result in punishment by the contract or the +/// other validators. +#[derive(Debug, Display)] +pub enum PhaseError { + /// The smart contract reported a phase as both commitment and reveal phase. + #[display(fmt = "Inconsistent randomness phase information")] + PhaseConflict, + /// The smart contract reported that we already revealed something while still being in the + /// commit phase. + #[display(fmt = "Revealed during commit phase")] + RevealedInCommit, + /// Failed to load contract information. + #[display(fmt = "Error loading randomness contract information: {:?}", _0)] + LoadFailed(CallError), + /// Failed to load the stored secret. + #[display(fmt = "Failed to load secret from the randomness contract")] + StaleSecret, + /// Failed to encrypt stored secret. + #[display(fmt = "Failed to encrypt stored randomness secret: {}", _0)] + Crypto(ethkey::crypto::Error), + /// Failed to decrypt stored secret. + #[display(fmt = "Failed to decrypt stored randomness secret: {}", _0)] + Decrypt(ethkey::crypto::Error), + /// Failed to get the engine signer's public key. + #[display(fmt = "Failed to get the engine signer's public key")] + MissingPublicKey, +} + +impl RandomnessPhase { + /// Determine randomness generation state from the contract. + /// + /// Calls various constant contract functions to determine the precise state that needs to be + /// handled (that is, the phase and whether or not the current validator still needs to send + /// commitments or reveal secrets). + pub fn load( + contract: &BoundContract, + our_address: Address, + ) -> Result { + // Determine the current round and which phase we are in. + let round = contract + .call_const(aura_random::functions::current_collect_round::call()) + .map_err(PhaseError::LoadFailed)?; + let is_reveal_phase = contract + .call_const(aura_random::functions::is_reveal_phase::call()) + .map_err(PhaseError::LoadFailed)?; + let is_commit_phase = contract + .call_const(aura_random::functions::is_commit_phase::call()) + .map_err(PhaseError::LoadFailed)?; + + // Ensure we are not committing or revealing twice. + let committed = contract + .call_const(aura_random::functions::is_committed::call( + round, + our_address, + )) + .map_err(PhaseError::LoadFailed)?; + let revealed: bool = contract + .call_const(aura_random::functions::sent_reveal::call( + round, + our_address, + )) + .map_err(PhaseError::LoadFailed)?; + + // With all the information known, we can determine the actual state we are in. + if is_reveal_phase && is_commit_phase { + return Err(PhaseError::PhaseConflict); + } + + if is_commit_phase { + if revealed { + return Err(PhaseError::RevealedInCommit); + } + + if !committed { + Ok(RandomnessPhase::BeforeCommit { our_address, round }) + } else { + Ok(RandomnessPhase::Committed) + } + } else { + if !committed { + // We apparently entered too late to make a commitment, wait until we get a chance again. + return Ok(RandomnessPhase::Waiting); + } + + if !revealed { + Ok(RandomnessPhase::Reveal { our_address, round }) + } else { + Ok(RandomnessPhase::Waiting) + } + } + } + + /// Advance the randomness state, if necessary. + /// + /// Returns the contract calls necessary to advance the randomness contract's state. + /// + /// **Warning**: The `advance()` function should be called only once per block state; otherwise + /// spurious transactions resulting in punishments might be executed. + pub fn advance( + self, + contract: &BoundContract, + rng: &mut R, + signer: &dyn EngineSigner, + ) -> Result, PhaseError> { + match self { + RandomnessPhase::Waiting | RandomnessPhase::Committed => Ok(None), + RandomnessPhase::BeforeCommit { round, our_address } => { + // Check whether a secret has already been committed in this round. + let committed_hash: Hash = contract + .call_const(aura_random::functions::get_commit::call(round, our_address)) + .map_err(PhaseError::LoadFailed)?; + if !committed_hash.is_zero() { + return Ok(None); // Already committed. + } + + // Generate a new secret. Compute the secret's hash, and encrypt the secret to ourselves. + let secret: Secret = rng.gen(); + let secret_hash: Hash = keccak(secret.as_ref()); + let public = signer.public().ok_or(PhaseError::MissingPublicKey)?; + let cipher = ecies::encrypt(&public, &secret_hash.0, &secret).map_err(PhaseError::Crypto)?; + + trace!(target: "engine", "Randomness contract: committing {}.", secret_hash); + // Schedule the transaction that commits the hash and the encrypted secret. + let (data, _decoder) = aura_random::functions::commit_hash::call(secret_hash, cipher); + Ok(Some(data)) + } + RandomnessPhase::Reveal { round, our_address } => { + // Load the hash and encrypted secret that we stored in the commit phase. + let committed_hash: Hash = contract + .call_const(aura_random::functions::get_commit::call(round, our_address)) + .map_err(PhaseError::LoadFailed)?; + let cipher = contract + .call_const(aura_random::functions::get_cipher::call(round, our_address)) + .map_err(PhaseError::LoadFailed)?; + + // Decrypt the secret and check against the hash. + let secret_vec = signer.decrypt(&committed_hash.0, &cipher).map_err(PhaseError::Decrypt)?; + let secret = if secret_vec.len() == 32 { + let mut buf = [0u8; 32]; + buf.copy_from_slice(&secret_vec); + buf + } else { + // This can only happen if there is a bug in the smart contract, + // or if the entire network goes awry. + error!(target: "engine", "Decrypted randomness secret has the wrong length."); + return Err(PhaseError::StaleSecret); + }; + let secret_hash: Hash = keccak(secret.as_ref()); + if secret_hash != committed_hash { + error!(target: "engine", "Decrypted randomness secret doesn't agree with the hash."); + return Err(PhaseError::StaleSecret); + } + + trace!(target: "engine", "Randomness contract: revealing secret for {}.", secret_hash); + // We are now sure that we have the correct secret and can reveal it. + let (data, _decoder) = aura_random::functions::reveal_secret::call(secret); + Ok(Some(data)) + } + } + } +} diff --git a/ethcore/engines/authority-round/src/util.rs b/ethcore/engines/authority-round/src/util.rs new file mode 100644 index 00000000000..d5ed914243e --- /dev/null +++ b/ethcore/engines/authority-round/src/util.rs @@ -0,0 +1,78 @@ +//! Utility functions. +//! +//! Contains small functions used by the AuRa engine that are not strictly limited to that scope. + +use std::fmt; + +use client_traits::EngineClient; +use common_types::ids::BlockId; +use ethabi; +use ethereum_types::Address; + +/// A contract bound to a client and block number. +/// +/// A bound contract is a combination of a `Client` reference, a `BlockId` and a contract `Address`. +/// These three parts are enough to call a contract's function; return values are automatically +/// decoded. +pub struct BoundContract<'a> { + client: &'a dyn EngineClient, + block_id: BlockId, + contract_addr: Address, +} + +/// Contract call failed error. +#[derive(Debug)] +pub enum CallError { + /// The call itself failed. + CallFailed(String), + /// Decoding the return value failed or the decoded value was a failure. + DecodeFailed(ethabi::Error), + /// The passed in client reference could not be upgraded to a `BlockchainClient`. + NotFullClient, +} + +impl<'a> fmt::Debug for BoundContract<'a> { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.debug_struct("BoundContract") + .field("client", &(self.client as *const dyn EngineClient)) + .field("block_id", &self.block_id) + .field("contract_addr", &self.contract_addr) + .finish() + } +} + +impl<'a> BoundContract<'a> { + /// Create a new `BoundContract`. + #[inline] + pub fn bind(client: &dyn EngineClient, block_id: BlockId, contract_addr: Address) -> BoundContract { + BoundContract { + client, + block_id, + contract_addr, + } + } + + /// Perform a function call to an ethereum machine that doesn't create a transaction or change the state. + /// + /// Runs a constant function call on `client`. The `call` value can be serialized by calling any + /// api function generated by the `use_contract!` macro. This does not create any transactions, it only produces a + /// result based on the state at the current block. + pub fn call_const(&self, call: (ethabi::Bytes, D)) -> Result + where + D: ethabi::FunctionOutputDecoder, + { + let (data, output_decoder) = call; + + let call_return = self + .client + .as_full_client() + .ok_or(CallError::NotFullClient)? + .call_contract(self.block_id, self.contract_addr, data) + .map_err(CallError::CallFailed)?; + + // Decode the result and return it. + output_decoder + .decode(call_return.as_slice()) + .map_err(CallError::DecodeFailed) + } +} diff --git a/ethcore/engines/validator-set/src/multi.rs b/ethcore/engines/validator-set/src/multi.rs index 72b653041c6..1990d7d835f 100644 --- a/ethcore/engines/validator-set/src/multi.rs +++ b/ethcore/engines/validator-set/src/multi.rs @@ -165,7 +165,7 @@ mod tests { use engine::EpochChange; use ethcore::{ miner::{self, MinerService}, - test_helpers::{generate_dummy_client_with_spec, generate_dummy_client_with_spec_and_data}, + test_helpers::generate_dummy_client_with_spec, }; use ethereum_types::Address; use ethkey::Secret; @@ -212,7 +212,7 @@ mod tests { assert_eq!(client.chain_info().best_block_number, 3); // Check syncing. - let sync_client = generate_dummy_client_with_spec_and_data(spec::new_validator_multi, 0, 0, &[]); + let sync_client = generate_dummy_client_with_spec(spec::new_validator_multi); sync_client.engine().register_client(Arc::downgrade(&sync_client) as _); for i in 1..4 { sync_client.import_block(Unverified::from_rlp(client.block(BlockId::Number(i)).unwrap().into_inner()).unwrap()).unwrap(); diff --git a/ethcore/engines/validator-set/src/safe_contract.rs b/ethcore/engines/validator-set/src/safe_contract.rs index d023802f0bf..521cefa8980 100644 --- a/ethcore/engines/validator-set/src/safe_contract.rs +++ b/ethcore/engines/validator-set/src/safe_contract.rs @@ -468,7 +468,7 @@ mod tests { use engine::{EpochChange, Proof}; use ethcore::{ miner::{self, MinerService}, - test_helpers::{generate_dummy_client_with_spec, generate_dummy_client_with_spec_and_data} + test_helpers::generate_dummy_client_with_spec }; use ethkey::Secret; use ethereum_types::Address; @@ -551,7 +551,7 @@ mod tests { assert_eq!(client.chain_info().best_block_number, 3); // Check syncing. - let sync_client = generate_dummy_client_with_spec_and_data(spec::new_validator_safe_contract, 0, 0, &[]); + let sync_client = generate_dummy_client_with_spec(spec::new_validator_safe_contract); sync_client.engine().register_client(Arc::downgrade(&sync_client) as _); for i in 1..4 { sync_client.import_block(Unverified::from_rlp(client.block(BlockId::Number(i)).unwrap().into_inner()).unwrap()).unwrap(); diff --git a/ethcore/res/authority_round_randomness_contract.json b/ethcore/res/authority_round_randomness_contract.json new file mode 100644 index 00000000000..0ee53150bee --- /dev/null +++ b/ethcore/res/authority_round_randomness_contract.json @@ -0,0 +1,60 @@ +{ + "name": "TestAuthorityRoundRandomnessContract", + "engine": { + "authorityRound": { + "params": { + "stepDuration": 1, + "startStep": 2, + "validators": { + "list": [ + "0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e" + ] + }, + "immediateTransitions": true, + "maximumEmptySteps": "2", + "randomnessContractAddress": "0x0000000000000000000000000000000000000042" + } + } + }, + "params": { + "gasLimitBoundDivisor": "0x0400", + "accountStartNonce": "0x0", + "maximumExtraDataSize": "0x20", + "minGasLimit": "0x1388", + "networkID" : "0x69", + "eip140Transition": "0x0", + "eip211Transition": "0x0", + "eip214Transition": "0x0", + "eip658Transition": "0x0" + }, + "genesis": { + "seal": { + "authorityRound": { + "step": "0x0", + "signature": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } + }, + "difficulty": "0x20000", + "author": "0x0000000000000000000000000000000000000000", + "timestamp": "0x00", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData": "0x", + "gasLimit": "0x222222" + }, + "accounts": { + "0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e": { "balance": "100000000000" }, + "0000000000000000000000000000000000000001": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, + "0000000000000000000000000000000000000002": { "balance": "1", "nonce": "1048576", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } }, + "0000000000000000000000000000000000000003": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } }, + "0000000000000000000000000000000000000004": { "balance": "1", "nonce": "1048576", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } }, + "0000000000000000000000000000000000000005": { "balance": "1", "builtin": { "name": "modexp", "activate_at": 0, "pricing": { "modexp": { "divisor": 20 } } } }, + "0000000000000000000000000000000000000006": { "balance": "1", "builtin": { "name": "alt_bn128_add", "activate_at": 0, "pricing": { "linear": { "base": 500, "word": 0 } } } }, + "0000000000000000000000000000000000000007": { "balance": "1", "builtin": { "name": "alt_bn128_mul", "activate_at": 0, "pricing": { "linear": { "base": 40000, "word": 0 } } } }, + "0000000000000000000000000000000000000008": { "balance": "1", "builtin": { "name": "alt_bn128_pairing", "activate_at": 0, "pricing": { "alt_bn128_pairing": { "base": 100000, "pair": 80000 } } } }, + "9cce34f7ab185c7aba1b7c8140d620b4bda941d6": { "balance": "1606938044258990275541962092341162602522202993782792835301376", "nonce": "1048576" }, + "0000000000000000000000000000000000000042": { + "balance": "1", + "constructor": "608060405234801561001057600080fd5b506107dd806100206000396000f3fe608060405234801561001057600080fd5b5060043610610107576000357c01000000000000000000000000000000000000000000000000000000009004806363f160e6116100a957806398df67c61161008357806398df67c61461031c578063baf11cab14610339578063c358ced014610365578063f2f56ffe1461036d57610107565b806363f160e6146102cc57806374ce90671461030c5780637a3e286b1461031457610107565b806320965255116100e557806320965255146102525780632e8a8dd51461026c5780633fa4f245146102985780635580e58b146102a057610107565b806304fdb0161461010c578063096a113d146101ad5780630b61ba85146101d9575b600080fd5b6101386004803603604081101561012257600080fd5b5080359060200135600160a060020a0316610399565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561017257818101518382015260200161015a565b50505050905090810190601f16801561019f5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b610138600480360360408110156101c357600080fd5b5080359060200135600160a060020a031661043d565b610250600480360360408110156101ef57600080fd5b8135919081019060408101602082013564010000000081111561021157600080fd5b82018360208201111561022357600080fd5b8035906020019184600183028401116401000000008311171561024557600080fd5b5090925090506104ef565b005b61025a610574565b60408051918252519081900360200190f35b61025a6004803603604081101561028257600080fd5b5080359060200135600160a060020a031661057b565b61025a610595565b61025a600480360360408110156102b657600080fd5b5080359060200135600160a060020a031661059b565b6102f8600480360360408110156102e257600080fd5b5080359060200135600160a060020a03166105b8565b604080519115158252519081900360200190f35b6102f86105e2565b61025a6105f2565b6102506004803603602081101561033257600080fd5b50356105fd565b6102f86004803603604081101561034f57600080fd5b5080359060200135600160a060020a03166106ab565b6102f86106c1565b61025a6004803603604081101561038357600080fd5b5080359060200135600160a060020a03166106cc565b600160208181526000938452604080852082529284529282902080548351600293821615610100026000190190911692909204601f810185900485028301850190935282825290929091908301828280156104355780601f1061040a57610100808354040283529160200191610435565b820191906000526020600020905b81548152906001019060200180831161041857829003601f168201915b505050505081565b6000828152600160208181526040808420600160a060020a038616855282529283902080548451600294821615610100026000190190911693909304601f810183900483028401830190945283835260609390918301828280156104e25780601f106104b7576101008083540402835291602001916104e2565b820191906000526020600020905b8154815290600101906020018083116104c557829003601f168201915b5050505050905092915050565b4133146104fb57600080fd5b610507600143036106f2565b61051057600080fd5b600061051e600143036106fd565b905061052a81336106ab565b1561053457600080fd5b6000818152602081815260408083203380855290835281842088905584845260018352818420908452909152902061056d908484610710565b5050505050565b6003545b90565b600060208181529281526040808220909352908152205481565b60035481565b600260209081526000928352604080842090915290825290205481565b6000918252600260209081526040808420600160a060020a03939093168452919052902054151590565b60006105ed436106f2565b905090565b60006105ed436106fd565b41331461060957600080fd5b61061560014303610704565b61061e57600080fd5b600061062c600143036106fd565b905061063881336105b8565b1561064257600080fd5b60408051602080820185905282518083038201815291830183528151918101919091206000848152808352838120338252909252919020541461068457600080fd5b60009081526002602090815260408083203384529091529020819055600380549091189055565b6000806106b884846106cc565b14159392505050565b60006105ed43610704565b600091825260208281526040808420600160a060020a0393909316845291905290205490565b600360069091061090565b6006900490565b60036006909106101590565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106107515782800160ff1982351617855561077e565b8280016001018555821561077e579182015b8281111561077e578235825591602001919060010190610763565b5061078a92915061078e565b5090565b61057891905b8082111561078a576000815560010161079456fea265627a7a72305820883fbcdb4e45da5d878f68564910eee8ea01c855ad862aee3f5162e50e06e9f564736f6c634300050a0032" + } + } +} diff --git a/ethcore/res/contracts/authority_round_random.json b/ethcore/res/contracts/authority_round_random.json new file mode 100644 index 00000000000..1be1c20e38f --- /dev/null +++ b/ethcore/res/contracts/authority_round_random.json @@ -0,0 +1,158 @@ +[{ + "constant": true, + "inputs": [], + "name": "currentRandom", + "outputs": [{ + "name": "", + "type": "uint256[]" + }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [{ + "name": "_secretHash", + "type": "bytes32" + }, + { + "name": "_cipher", + "type": "bytes" + } + ], + "name": "commitHash", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ + "name": "_secret", + "type": "uint256" + }], + "name": "revealSecret", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "currentCollectRound", + "outputs": [{ + "name": "", + "type": "uint256" + }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ + "name": "_collectRound", + "type": "uint256" + }, + { + "name": "_validator", + "type": "address" + } + ], + "name": "getCipher", + "outputs": [{ + "name": "", + "type": "bytes" + }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ + "name": "_collectRound", + "type": "uint256" + }, + { + "name": "_validator", + "type": "address" + } + ], + "name": "getCommit", + "outputs": [{ + "name": "", + "type": "bytes32" + }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ + "name": "_collectRound", + "type": "uint256" + }, + { + "name": "_validator", + "type": "address" + } + ], + "name": "isCommitted", + "outputs": [{ + "name": "", + "type": "bool" + }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isCommitPhase", + "outputs": [{ + "name": "", + "type": "bool" + }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isRevealPhase", + "outputs": [{ + "name": "", + "type": "bool" + }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ + "name": "_collectRound", + "type": "uint256" + }, + { + "name": "_validator", + "type": "address" + } + ], + "name": "sentReveal", + "outputs": [{ + "name": "", + "type": "bool" + }], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/ethcore/res/contracts/test_authority_round_random.json b/ethcore/res/contracts/test_authority_round_random.json new file mode 100644 index 00000000000..31528eeca5b --- /dev/null +++ b/ethcore/res/contracts/test_authority_round_random.json @@ -0,0 +1,265 @@ +[ + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "uint256" + }, + { + "name": "", + "type": "address" + } + ], + "name": "ciphers", + "outputs": [ + { + "name": "", + "type": "bytes" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_collectRound", + "type": "uint256" + }, + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "getCipher", + "outputs": [ + { + "name": "", + "type": "bytes" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_secretHash", + "type": "bytes32" + }, + { + "name": "_cipher", + "type": "bytes" + } + ], + "name": "commitHash", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getValue", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "uint256" + }, + { + "name": "", + "type": "address" + } + ], + "name": "hashes", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "value", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "uint256" + }, + { + "name": "", + "type": "address" + } + ], + "name": "secrets", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_collectRound", + "type": "uint256" + }, + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "sentReveal", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isCommitPhase", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_secret", + "type": "uint256" + } + ], + "name": "revealSecret", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getRound", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_collectRound", + "type": "uint256" + }, + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "isCommitted", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isRevealPhase", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_collectRound", + "type": "uint256" + }, + { + "name": "_miningAddress", + "type": "address" + } + ], + "name": "getCommit", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/ethcore/res/contracts/test_authority_round_random.sol b/ethcore/res/contracts/test_authority_round_random.sol new file mode 100644 index 00000000000..9fa253e18c4 --- /dev/null +++ b/ethcore/res/contracts/test_authority_round_random.sol @@ -0,0 +1,107 @@ +pragma solidity 0.5.10; + +/// @dev Randomness test contract based on https://github.com/poanetwork/posdao-contracts. +/// Generates and stores random numbers in a RANDAO manner and accumulates a random seed. +contract Random { + mapping(uint256 => mapping(address => bytes32)) public hashes; + mapping(uint256 => mapping(address => bytes)) public ciphers; + mapping(uint256 => mapping(address => uint256)) public secrets; + uint256 public value; + + /// @dev Called by the validator's node to store a hash and a cipher of the validator's secret on each collection + /// round. The validator's node must use its mining address to call this function. + /// This function can only be called once per collection round (during the `commits phase`). + /// @param _secretHash The Keccak-256 hash of the validator's secret. + /// @param _cipher The cipher of the validator's secret. Can be used by the node to decrypt and reveal. + function commitHash(bytes32 _secretHash, bytes calldata _cipher) external { + require(block.coinbase == msg.sender); + require(_isCommitPhase(block.number - 1)); + uint256 round = _collectRound(block.number - 1); + require(!isCommitted(round, msg.sender)); + hashes[round][msg.sender] = _secretHash; + ciphers[round][msg.sender] = _cipher; + } + + /// @dev Called by the validator's node to XOR its secret with the current random seed. + /// The validator's node must use its mining address to call this function. + /// This function can only be called once per collection round (during the `reveals phase`). + /// @param _secret The validator's secret. + function revealSecret(uint256 _secret) external { + require(block.coinbase == msg.sender); + require(_isRevealPhase(block.number - 1)); + uint256 round = _collectRound(block.number - 1); + require(!sentReveal(round, msg.sender)); + require(hashes[round][msg.sender] == keccak256(abi.encodePacked(_secret))); + secrets[round][msg.sender] = _secret; + value ^= _secret; + } + + /// @dev Returns the cipher of the validator's secret for the specified collection round and the specified validator + /// stored by the validator through the `commitHash` function. + /// @param _collectRound The serial number of the collection round for which the cipher should be retrieved. + /// @param _miningAddress The mining address of validator. + function getCipher(uint256 _collectRound, address _miningAddress) public view returns(bytes memory) { + return ciphers[_collectRound][_miningAddress]; + } + + /// @dev Returns the Keccak-256 hash of the validator's secret for the specified collection round and the specified + /// validator stored by the validator through the `commitHash` function. + /// @param _collectRound The serial number of the collection round for which the hash should be retrieved. + /// @param _miningAddress The mining address of validator. + function getCommit(uint256 _collectRound, address _miningAddress) public view returns(bytes32) { + return hashes[_collectRound][_miningAddress]; + } + + /// @dev Returns a boolean flag indicating whether the specified validator has committed their secret's hash for the + /// specified collection round. + /// @param _collectRound The serial number of the collection round for which the checkup should be done. + /// @param _miningAddress The mining address of the validator. + function isCommitted(uint256 _collectRound, address _miningAddress) public view returns(bool) { + return getCommit(_collectRound, _miningAddress) != bytes32(0); + } + + /// @dev Returns a boolean flag indicating whether the current phase of the current collection round + /// is a `commits phase`. Used by the validator's node to determine if it should commit the hash of + /// the secret during the current collection round. + function isCommitPhase() public view returns(bool) { + return _isCommitPhase(block.number); + } + + /// @dev Returns a boolean flag indicating whether the current phase of the current collection round + /// is a `reveals phase`. Used by the validator's node to determine if it should reveal the secret during + /// the current collection round. + function isRevealPhase() public view returns(bool) { + return _isRevealPhase(block.number); + } + + /// @dev Returns a boolean flag of whether the specified validator has revealed their secret for the + /// specified collection round. + /// @param _collectRound The serial number of the collection round for which the checkup should be done. + /// @param _miningAddress The mining address of the validator. + function sentReveal(uint256 _collectRound, address _miningAddress) public view returns(bool) { + return secrets[_collectRound][_miningAddress] != uint256(0); + } + + /// @dev Returns the current collect round number. + function currentCollectRound() public view returns(uint256) { + return _collectRound(block.number); + } + + /// @dev Returns the current random value. + function getValue() public view returns(uint256) { + return value; + } + + function _collectRound(uint256 blockNumber) private pure returns(uint256) { + return blockNumber / 6; + } + + function _isCommitPhase(uint256 blockNumber) private pure returns(bool) { + return blockNumber % 6 < 3; + } + + function _isRevealPhase(uint256 blockNumber) private pure returns(bool) { + return blockNumber % 6 >= 3; + } +} + diff --git a/ethcore/spec/src/chain.rs b/ethcore/spec/src/chain.rs index db4e2054040..0cc9cf17930 100644 --- a/ethcore/spec/src/chain.rs +++ b/ethcore/spec/src/chain.rs @@ -82,6 +82,7 @@ bundle_test_spec! { "authority_round" => new_test_round, "authority_round_block_reward_contract" => new_test_round_block_reward_contract, "authority_round_empty_steps" => new_test_round_empty_steps, + "authority_round_randomness_contract" => new_test_round_randomness_contract, "constructor" => new_test_constructor, "ethereum/byzantium_test" => new_byzantium_test, "ethereum/constantinople_test" => new_constantinople_test, diff --git a/ethcore/src/client/client.rs b/ethcore/src/client/client.rs index 60e4263aefc..dc71fb455c9 100644 --- a/ethcore/src/client/client.rs +++ b/ethcore/src/client/client.rs @@ -2216,29 +2216,42 @@ impl BlockChainClient for Client { } } - fn transact_contract(&self, address: Address, data: Bytes) -> Result<(), transaction::Error> { + fn create_transaction( + &self, + action: Action, + data: Bytes, + gas: Option, + gas_price: Option, + nonce: Option + ) -> Result { let authoring_params = self.importer.miner.authoring_params(); let service_transaction_checker = self.importer.miner.service_transaction_checker(); let gas_price = if let Some(checker) = service_transaction_checker { match checker.check_address(self, authoring_params.author) { Ok(true) => U256::zero(), - _ => self.importer.miner.sensible_gas_price(), + _ => gas_price.unwrap_or_else(|| self.importer.miner.sensible_gas_price()), } } else { self.importer.miner.sensible_gas_price() }; let transaction = transaction::Transaction { - nonce: self.latest_nonce(&authoring_params.author), - action: Action::Call(address), - gas: self.importer.miner.sensible_gas_limit(), + nonce: nonce.unwrap_or_else(|| self.latest_nonce(&authoring_params.author)), + action, + gas: gas.unwrap_or_else(|| self.importer.miner.sensible_gas_limit()), gas_price, value: U256::zero(), - data: data, + data, }; let chain_id = self.engine.signing_chain_id(&self.latest_env_info()); let signature = self.engine.sign(transaction.hash(chain_id)) .map_err(|e| transaction::Error::InvalidSignature(e.to_string()))?; - let signed = SignedTransaction::new(transaction.with_signature(signature, chain_id))?; + Ok(SignedTransaction::new(transaction.with_signature(signature, chain_id))?) + } + + fn transact(&self, action: Action, data: Bytes, gas: Option, gas_price: Option, nonce: Option) + -> Result<(), transaction::Error> + { + let signed = self.create_transaction(action, data, gas, gas_price, nonce)?; self.importer.miner.import_own_transaction(self, signed.into()) } diff --git a/ethcore/src/client/test_client.rs b/ethcore/src/client/test_client.rs index 6f243cb4c45..34c00e80458 100644 --- a/ethcore/src/client/test_client.rs +++ b/ethcore/src/client/test_client.rs @@ -898,18 +898,37 @@ impl BlockChainClient for TestBlockChainClient { } } - fn transact_contract(&self, address: Address, data: Bytes) -> Result<(), transaction::Error> { + fn create_transaction( + &self, + action: Action, + data: Bytes, + gas: Option, + gas_price: Option, + nonce: Option + ) -> Result { let transaction = Transaction { - nonce: self.latest_nonce(&self.miner.authoring_params().author), - action: Action::Call(address), - gas: self.spec.gas_limit, - gas_price: U256::zero(), + nonce: nonce.unwrap_or_else(|| self.latest_nonce(&self.miner.authoring_params().author)), + action, + gas: gas.unwrap_or(self.spec.gas_limit), + gas_price: gas_price.unwrap_or_else(U256::zero), value: U256::default(), data: data, }; let chain_id = Some(self.spec.chain_id()); let sig = self.spec.engine.sign(transaction.hash(chain_id)).unwrap(); - let signed = SignedTransaction::new(transaction.with_signature(sig, chain_id)).unwrap(); + Ok(SignedTransaction::new(transaction.with_signature(sig, chain_id)).unwrap()) + } + + fn transact( + &self, + action: Action, + data: Bytes, + gas: Option, + gas_price: Option, + _nonce: Option, + ) -> Result<(), transaction::Error> + { + let signed = self.create_transaction(action, data, gas, gas_price, None)?; self.miner.import_own_transaction(self, signed.into()) } diff --git a/ethcore/src/lib.rs b/ethcore/src/lib.rs index a34db6c6522..c1b4dfebcb9 100644 --- a/ethcore/src/lib.rs +++ b/ethcore/src/lib.rs @@ -112,7 +112,7 @@ extern crate rand_xorshift; extern crate ethcore_accounts as accounts; #[cfg(feature = "stratum")] extern crate ethcore_stratum; -#[cfg(any(test, feature = "stratum"))] +#[cfg(feature = "stratum")] extern crate ethash; #[cfg(any(test, feature = "test-helpers"))] @@ -126,7 +126,7 @@ extern crate kvdb_rocksdb; #[cfg(any(test, feature = "json-tests"))] #[macro_use] extern crate lazy_static; -#[cfg(any(test, feature = "test-helpers"))] +#[cfg(any(test, feature = "json-tests"))] #[macro_use] extern crate macros; #[cfg(test)] diff --git a/ethcore/src/miner/miner.rs b/ethcore/src/miner/miner.rs index bd12ede1965..e6c28530142 100644 --- a/ethcore/src/miner/miner.rs +++ b/ethcore/src/miner/miner.rs @@ -332,6 +332,13 @@ impl Miner { /// /// NOTE This should be only used for tests. pub fn new_for_tests(spec: &Spec, accounts: Option>) -> Miner { + Miner::new_for_tests_force_sealing(spec, accounts, false) + } + + /// Creates new instance of miner with given spec and accounts. + /// + /// NOTE This should be only used for tests. + pub fn new_for_tests_force_sealing(spec: &Spec, accounts: Option>, force_sealing: bool) -> Miner { let minimal_gas_price = 0.into(); Miner::new(MinerOptions { pool_verification_options: pool::verifier::Options { @@ -341,6 +348,7 @@ impl Miner { no_early_reject: false, }, reseal_min_period: Duration::from_secs(0), + force_sealing, ..Default::default() }, GasPricer::new_fixed(minimal_gas_price), spec, accounts.unwrap_or_default()) } @@ -425,9 +433,11 @@ impl Miner { { trace_time!("prepare_block"); let chain_info = chain.chain_info(); + let engine_pending; + let mut open_block; // Open block - let (mut open_block, original_work_hash) = { + let original_work_hash = { let mut sealing = self.sealing.lock(); let last_work_hash = sealing.queue.peek_last_ref().map(|pb| pb.header.hash()); let best_hash = chain_info.best_block_hash; @@ -438,36 +448,49 @@ impl Miner { // if at least one was pushed successfully, close and enqueue new ClosedBlock; // otherwise, leave everything alone. // otherwise, author a fresh block. - let mut open_block = match sealing.queue.get_pending_if(|b| b.header.parent_hash() == &best_hash) { + match sealing.queue.get_pending_if(|b| b.header.parent_hash() == &best_hash) { Some(old_block) => { trace!(target: "miner", "prepare_block: Already have previous work; updating and returning"); + engine_pending = Vec::new(); // add transactions to old_block - chain.reopen_block(old_block) + open_block = chain.reopen_block(old_block); } None => { // block not found - create it. trace!(target: "miner", "prepare_block: No existing work - making new block"); let params = self.params.read().clone(); - match chain.prepare_open_block( + open_block = match chain.prepare_open_block( params.author, params.gas_range_target, params.extra_data, ) { Ok(block) => block, Err(err) => { - warn!(target: "miner", "Open new block failed with error {:?}. This is likely an error in chain specificiations or on-chain consensus smart contracts.", err); + warn!(target: "miner", "Open new block failed with error {:?}. This is likely an error in \ + chain specification or on-chain consensus smart contracts.", err); return None; } - } + }; + + // Before adding from the queue to the new block, give the engine a chance to add transactions. + engine_pending = match self.engine.on_prepare_block(&open_block) { + Ok(transactions) => transactions, + Err(err) => { + error!(target: "miner", "Failed to prepare engine transactions for new block: {:?}. \ + This is likely an error in chain specification or on-chain consensus smart \ + contracts.", err); + return None; + } + }; } - }; + } if self.options.infinite_pending_block { open_block.remove_gas_limit(); } - (open_block, last_work_hash) + last_work_hash }; let mut invalid_transactions = HashSet::new(); @@ -493,13 +516,13 @@ impl Miner { MAX_SKIPPED_TRANSACTIONS.saturating_add(cmp::min(*open_block.header.gas_limit() / min_tx_gas, u64::max_value().into()).as_u64() as usize) }; - let pending: Vec> = self.transaction_queue.pending( + let queue_pending: Vec> = self.transaction_queue.pending( client.clone(), pool::PendingSettings { block_number: chain_info.best_block_number, current_timestamp: chain_info.best_block_timestamp, nonce_cap, - max_len: max_transactions, + max_len: max_transactions.saturating_sub(engine_pending.len()), ordering: miner::PendingOrdering::Priority, } ); @@ -509,12 +532,11 @@ impl Miner { }; let block_start = Instant::now(); - debug!(target: "miner", "Attempting to push {} transactions.", pending.len()); + debug!(target: "miner", "Attempting to push {} transactions.", engine_pending.len() + queue_pending.len()); - for tx in pending { + for transaction in engine_pending.into_iter().chain(queue_pending.into_iter().map(|tx| tx.signed().clone())) { let start = Instant::now(); - let transaction = tx.signed().clone(); let hash = transaction.hash(); let sender = transaction.sender(); diff --git a/ethcore/src/snapshot/service.rs b/ethcore/src/snapshot/service.rs index cdc0d6a1888..3af6af2aed6 100644 --- a/ethcore/src/snapshot/service.rs +++ b/ethcore/src/snapshot/service.rs @@ -918,7 +918,7 @@ mod tests { #[test] fn sends_async_messages() { let gas_prices = vec![1.into(), 2.into(), 3.into(), 999.into()]; - let client = generate_dummy_client_with_spec_and_data(spec::new_null, 400, 5, &gas_prices); + let client = generate_dummy_client_with_spec_and_data(spec::new_null, 400, 5, &gas_prices, false); let service = IoService::::start().unwrap(); let spec = spec::new_test(); diff --git a/ethcore/src/snapshot/tests/service.rs b/ethcore/src/snapshot/tests/service.rs index 5702ddf9703..e6a643e8cab 100644 --- a/ethcore/src/snapshot/tests/service.rs +++ b/ethcore/src/snapshot/tests/service.rs @@ -47,7 +47,7 @@ fn restored_is_equivalent() { const TX_PER: usize = 5; let gas_prices = vec![1.into(), 2.into(), 3.into(), 999.into()]; - let client = generate_dummy_client_with_spec_and_data(spec::new_null, NUM_BLOCKS, TX_PER, &gas_prices); + let client = generate_dummy_client_with_spec_and_data(spec::new_null, NUM_BLOCKS, TX_PER, &gas_prices, false); let tempdir = TempDir::new("").unwrap(); let client_db = tempdir.path().join("client_db"); @@ -111,7 +111,7 @@ fn restored_is_equivalent() { #[test] fn guards_delete_folders() { let gas_prices = vec![1.into(), 2.into(), 3.into(), 999.into()]; - let client = generate_dummy_client_with_spec_and_data(spec::new_null, 400, 5, &gas_prices); + let client = generate_dummy_client_with_spec_and_data(spec::new_null, 400, 5, &gas_prices, false); let spec = spec::new_null(); let tempdir = TempDir::new("").unwrap(); @@ -171,7 +171,7 @@ fn keep_ancient_blocks() { let gas_prices = vec![1.into(), 2.into(), 3.into(), 999.into()]; let spec_f = spec::new_null; let spec = spec_f(); - let client = generate_dummy_client_with_spec_and_data(spec_f, NUM_BLOCKS as u32, 5, &gas_prices); + let client = generate_dummy_client_with_spec_and_data(spec_f, NUM_BLOCKS as u32, 5, &gas_prices, false); let bc = client.chain(); @@ -280,7 +280,7 @@ fn recover_aborted_recovery() { const NUM_BLOCKS: u32 = 400; let gas_prices = vec![1.into(), 2.into(), 3.into(), 999.into()]; - let client = generate_dummy_client_with_spec_and_data(spec::new_null, NUM_BLOCKS, 5, &gas_prices); + let client = generate_dummy_client_with_spec_and_data(spec::new_null, NUM_BLOCKS, 5, &gas_prices, false); let spec = spec::new_null(); let tempdir = TempDir::new("").unwrap(); diff --git a/ethcore/src/test_helpers.rs b/ethcore/src/test_helpers.rs index 4ab63846a0a..d98a6ae169f 100644 --- a/ethcore/src/test_helpers.rs +++ b/ethcore/src/test_helpers.rs @@ -104,31 +104,35 @@ pub fn create_test_block_with_data(header: &Header, transactions: &[SignedTransa /// Generates dummy client (not test client) with corresponding amount of blocks pub fn generate_dummy_client(block_number: u32) -> Arc { - generate_dummy_client_with_spec_and_data(spec::new_test, block_number, 0, &[]) + generate_dummy_client_with_spec_and_data(spec::new_test, block_number, 0, &[], false) } /// Generates dummy client (not test client) with corresponding amount of blocks and txs per every block pub fn generate_dummy_client_with_data(block_number: u32, txs_per_block: usize, tx_gas_prices: &[U256]) -> Arc { - generate_dummy_client_with_spec_and_data(spec::new_null, block_number, txs_per_block, tx_gas_prices) + generate_dummy_client_with_spec_and_data(spec::new_null, block_number, txs_per_block, tx_gas_prices, false) } /// Generates dummy client (not test client) with corresponding spec and accounts pub fn generate_dummy_client_with_spec(test_spec: F) -> Arc where F: Fn() -> Spec { - generate_dummy_client_with_spec_and_data(test_spec, 0, 0, &[]) + generate_dummy_client_with_spec_and_data(test_spec, 0, 0, &[], false) } /// Generates dummy client (not test client) with corresponding amount of blocks, txs per block and spec -pub fn generate_dummy_client_with_spec_and_data(test_spec: F, block_number: u32, txs_per_block: usize, tx_gas_prices: &[U256]) -> Arc where +pub fn generate_dummy_client_with_spec_and_data( + test_spec: F, block_number: u32, txs_per_block: usize, tx_gas_prices: &[U256], force_sealing: bool, +) -> Arc where F: Fn() -> Spec { let test_spec = test_spec(); let client_db = new_db(); + let miner = Miner::new_for_tests_force_sealing(&test_spec, None, force_sealing); + let client = Client::new( ClientConfig::default(), &test_spec, client_db, - Arc::new(Miner::new_for_tests(&test_spec, None)), + Arc::new(miner), IoChannel::disconnected(), ).unwrap(); let test_engine = &*test_spec.engine; diff --git a/ethcore/types/src/errors/engine_error.rs b/ethcore/types/src/errors/engine_error.rs index 02cdd066b36..0b9996c1f7d 100644 --- a/ethcore/types/src/errors/engine_error.rs +++ b/ethcore/types/src/errors/engine_error.rs @@ -35,8 +35,16 @@ pub enum EngineError { BadSealFieldSize(OutOfBounds), /// Validation proof insufficient. InsufficientProof(String), + /// Randomness error in load method + RandomnessLoadError(String), + /// Randomness error in advance method + RandomnessAdvanceError(String), /// Failed system call. FailedSystemCall(String), + /// Failed to decode the result of a system call. + SystemCallResultDecoding(String), + /// The result of a system call is invalid. + SystemCallResultInvalid(String), /// Malformed consensus message. MalformedMessage(String), /// Requires client ref, but none registered. @@ -90,7 +98,11 @@ impl fmt::Display for EngineError { UnexpectedMessage => "This Engine should not be fed messages.".into(), BadSealFieldSize(ref oob) => format!("Seal field has an unexpected length: {}", oob), InsufficientProof(ref msg) => format!("Insufficient validation proof: {}", msg), + RandomnessLoadError(ref rerr) => format!("Randomness error in load(): {:?}", rerr), + RandomnessAdvanceError(ref rerr) => format!("Randomness error in advance(): {:?}", rerr), FailedSystemCall(ref msg) => format!("Failed to make system call: {}", msg), + SystemCallResultDecoding(ref msg) => format!("Failed to decode the result of a system call: {}", msg), + SystemCallResultInvalid(ref msg) => format!("The result of a system call is invalid: {}", msg), MalformedMessage(ref msg) => format!("Received malformed consensus message: {}", msg), RequiresClient => format!("Call requires client but none registered"), RequiresSigner => format!("Call requires signer but none registered"), diff --git a/json/src/spec/authority_round.rs b/json/src/spec/authority_round.rs index 615cc4ca546..2b719351100 100644 --- a/json/src/spec/authority_round.rs +++ b/json/src/spec/authority_round.rs @@ -96,6 +96,8 @@ pub struct AuthorityRoundParams { pub strict_empty_steps_transition: Option, /// First block for which a 2/3 quorum (instead of 1/2) is required. pub two_thirds_majority_transition: Option, + /// The random number contract's address, or a map of contract transitions. + pub randomness_contract_address: Option>, } /// Authority engine deserialization. @@ -106,6 +108,17 @@ pub struct AuthorityRound { pub params: AuthorityRoundParams, } +/// Either a single `T` value, or a map assigning block numbers to transitions to different values of type `T`. +#[derive(Debug, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(untagged)] +pub enum TransitionMap { + /// A single value for all block numbers. + Single(Address), + /// A map of transition block numbers to new values. + Transitions(BTreeMap), +} + #[cfg(test)] mod tests { use ethereum_types::{U256, H160}; @@ -128,7 +141,11 @@ mod tests { "validateStepTransition": 150, "blockReward": 5000000, "maximumUncleCountTransition": 10000000, - "maximumUncleCount": 5 + "maximumUncleCount": 5, + "randomnessContractAddress": { + "10": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "20": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } } }"#; @@ -142,6 +159,10 @@ mod tests { assert_eq!(deserialized.params.immediate_transitions, None); assert_eq!(deserialized.params.maximum_uncle_count_transition, Some(Uint(10_000_000.into()))); assert_eq!(deserialized.params.maximum_uncle_count, Some(Uint(5.into()))); - + assert_eq!(deserialized.params.randomness_contract_address.unwrap(), + super::TransitionMap::Transitions(vec![ + (Uint(10.into()), Address(H160::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap())), + (Uint(20.into()), Address(H160::from_str("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap())), + ].into_iter().collect())); } } diff --git a/rpc/src/v1/helpers/engine_signer.rs b/rpc/src/v1/helpers/engine_signer.rs index fd40dc95e24..7d01e304e24 100644 --- a/rpc/src/v1/helpers/engine_signer.rs +++ b/rpc/src/v1/helpers/engine_signer.rs @@ -41,8 +41,19 @@ impl engine::signer::EngineSigner for EngineSigner { } } + fn decrypt(&self, auth_data: &[u8], cipher: &[u8]) -> Result, ethkey::crypto::Error> { + self.accounts.decrypt(self.address, None, auth_data, cipher).map_err(|e| { + warn!("Unable to decrypt message: {:?}", e); + ethkey::crypto::Error::InvalidMessage + }) + } + fn address(&self) -> Address { self.address } + + fn public(&self) -> Option { + self.accounts.account_public(self.address, &self.password).ok() + } }