From 2b1d148ceb27f8df5cf434c509d7c1ff9d19b152 Mon Sep 17 00:00:00 2001 From: Andreas Fackler Date: Tue, 17 Dec 2019 11:34:14 +0100 Subject: [PATCH] Add randomness contract support to AuthorityRound. (#10946) * 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. * Address review comments for randomness contract. Co-Authored-By: David * Rename revealSecret to revealNumber * Update Randomness contract bytecode * Use H256, rename secret to random number. * Use get_commit_and_cipher * Clean up Miner::prepare_block. * Remove is_reveal_phase call. * Add more comments, require randomness contract map. * Simplify run_randomness_phase * Address review comments. * Remove Client::transact_contract. --- Cargo.lock | 6 + ethcore/Cargo.toml | 1 - ethcore/client-traits/src/lib.rs | 64 ++++- ethcore/engine/src/engine.rs | 10 +- ethcore/engine/src/signer.rs | 16 +- ethcore/engine/src/test_helpers.rs | 13 +- ethcore/engines/authority-round/Cargo.toml | 6 + ethcore/engines/authority-round/src/lib.rs | 118 +++++++- .../engines/authority-round/src/randomness.rs | 257 +++++++++++++++++ ethcore/engines/authority-round/src/util.rs | 94 +++++++ ethcore/engines/validator-set/src/contract.rs | 8 +- ethcore/engines/validator-set/src/multi.rs | 14 +- .../validator-set/src/safe_contract.rs | 4 +- .../authority_round_randomness_contract.json | 100 +++++++ .../res/contracts/authority_round_random.json | 133 +++++++++ .../test_authority_round_random.json | 265 ++++++++++++++++++ .../contracts/test_authority_round_random.sol | 102 +++++++ .../snapshot/snapshot-tests/src/service.rs | 10 +- ethcore/spec/src/chain.rs | 1 + ethcore/src/client/client.rs | 23 +- ethcore/src/miner/miner.rs | 50 ++-- ethcore/src/test_helpers/mod.rs | 14 +- ethcore/src/test_helpers/test_client.rs | 20 +- ethcore/types/src/errors/engine_error.rs | 6 + json/src/spec/authority_round.rs | 14 +- rpc/src/v1/helpers/engine_signer.rs | 13 +- 26 files changed, 1293 insertions(+), 69 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 3d2bf969a..644fb8654 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,8 +172,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 9.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ethabi-contract 9.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ethabi-derive 9.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "ethcore 1.12.0", "ethcore-accounts 0.1.0", "ethcore-io 1.12.0", @@ -186,8 +190,10 @@ 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)", "parity-crypto 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "rlp 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", "spec 0.1.0", diff --git a/ethcore/Cargo.toml b/ethcore/Cargo.toml index 2095cfd7e..f05d50b4b 100644 --- a/ethcore/Cargo.toml +++ b/ethcore/Cargo.toml @@ -73,7 +73,6 @@ blooms-db = { path = "../util/blooms-db" } criterion = "0.3" engine = { path = "./engine", features = ["test-helpers"] } env_logger = "0.5" -ethash = { path = "../ethash" } ethcore-accounts = { path = "../accounts" } ethcore-builtin = { path = "./builtin" } ethjson = { path = "../json", features = ["test-helpers"] } diff --git a/ethcore/client-traits/src/lib.rs b/ethcore/client-traits/src/lib.rs index 3683730b0..eca608c21 100644 --- a/ethcore/client-traits/src/lib.rs +++ b/ethcore/client-traits/src/lib.rs @@ -42,7 +42,7 @@ use common_types::{ pruning_info::PruningInfo, receipt::LocalizedReceipt, trace_filter::Filter as TraceFilter, - transaction::{self, LocalizedTransaction, CallError, SignedTransaction, UnverifiedTransaction}, + transaction::{self, Action, LocalizedTransaction, CallError, SignedTransaction, UnverifiedTransaction}, tree_route::TreeRoute, verification::{VerificationQueueInfo, Unverified}, }; @@ -395,8 +395,66 @@ pub trait BlockChainClient: /// Returns information about pruning/data availability. 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>; + /// Returns a transaction signed with the key configured in the engine signer. + fn create_transaction(&self, tx_request: TransactionRequest) -> Result; + + /// Schedule state-altering transaction to be executed on the next pending + /// block with the given gas and nonce parameters. + fn transact(&self, tx_request: TransactionRequest) -> Result<(), transaction::Error>; +} + +/// The data required for a `Client` to create a transaction. +/// +/// Gas limit, gas price, or nonce can be set explicitly, e.g. to create service +/// transactions with zero gas price, or sequences of transactions with consecutive nonces. +pub struct TransactionRequest { + pub action: Action, + pub data: Bytes, + pub gas: Option, + pub gas_price: Option, + pub nonce: Option, +} + +impl TransactionRequest { + /// Creates a request to call a contract at `address` with the specified call data. + pub fn call(address: Address, data: Bytes) -> TransactionRequest { + TransactionRequest { + action: Action::Call(address), + data, + gas: None, + gas_price: None, + nonce: None, + } + } + + /// Creates a request to create a new contract, with the specified bytecode. + pub fn create(data: Bytes) -> TransactionRequest { + TransactionRequest { + action: Action::Create, + data, + gas: None, + gas_price: None, + nonce: None, + } + } + + /// Sets a gas limit. If this is not specified, a sensible default is used. + pub fn gas(mut self, gas: U256) -> TransactionRequest { + self.gas = Some(gas); + self + } + + /// Sets a gas price. If this is not specified, a sensible default is used. + pub fn gas_price(mut self, gas_price: U256) -> TransactionRequest { + self.gas_price = Some(gas_price); + self + } + + /// Sets a nonce. If this is not specified, the appropriate latest nonce for the author is used. + pub fn nonce(mut self, nonce: U256) -> TransactionRequest { + self.nonce = Some(nonce); + self + } } /// resets the blockchain diff --git a/ethcore/engine/src/engine.rs b/ethcore/engine/src/engine.rs index 44f76624a..a25ab4a27 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,14 @@ 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 generate_engine_transactions(&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 baacf7c09..6cd5ddbde 100644 --- a/ethcore/engine/src/signer.rs +++ b/ethcore/engine/src/signer.rs @@ -17,7 +17,7 @@ //! A signer used by Engines which need to sign messages. use ethereum_types::{H256, Address}; -use parity_crypto::publickey::{Signature, KeyPair, Error}; +use parity_crypto::publickey::{ecies, Public, Signature, KeyPair, Error}; /// Everything that an Engine needs to sign messages. pub trait EngineSigner: Send + Sync { @@ -26,6 +26,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, Error>; + + /// The signer's public key, if available. + fn public(&self) -> Option; } /// Creates a new `EngineSigner` from given key pair. @@ -40,7 +46,15 @@ impl EngineSigner for Signer { parity_crypto::publickey::sign(self.0.secret(), &hash) } + fn decrypt(&self, auth_data: &[u8], cipher: &[u8]) -> Result, 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 6423590b3..127025b59 100644 --- a/ethcore/engine/src/test_helpers.rs +++ b/ethcore/engine/src/test_helpers.rs @@ -20,7 +20,7 @@ use std::sync::Arc; use ethereum_types::{Address, H256}; use ethkey::Password; -use parity_crypto::publickey::{Signature, Error}; +use parity_crypto::publickey::{Public, Signature, Error}; use log::warn; use accounts::{self, AccountProvider, SignError}; @@ -44,7 +44,18 @@ impl EngineSigner for (Arc, Address, Password) { } } + fn decrypt(&self, auth_data: &[u8], cipher: &[u8]) -> Result, Error> { + self.0.decrypt(self.1, None, auth_data, cipher).map_err(|e| { + warn!("Unable to decrypt message: {:?}", e); + 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 5bca87fb8..9e3011eb6 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 = "9.0.1" +ethabi-contract = "9.0.0" +ethabi-derive = "9.0.1" ethereum-types = "0.8.0" ethjson = { path = "../../../json" } parity-crypto = { version = "0.4.2", features = ["publickey"] } @@ -22,7 +26,9 @@ log = "0.4" lru-cache = "0.1" machine = { path = "../../machine" } macros = { path = "../../../util/macros" } +parity-bytes = "0.1" parking_lot = "0.9" +rand = "0.7" rlp = "0.4.0" time-utils = { path = "../../../util/time-utils" } unexpected = { path = "../../../util/unexpected" } diff --git a/ethcore/engines/authority-round/src/lib.rs b/ethcore/engines/authority-round/src/lib.rs index 14654bc95..2a9ee7eea 100644 --- a/ethcore/engines/authority-round/src/lib.rs +++ b/ethcore/engines/authority-round/src/lib.rs @@ -40,7 +40,7 @@ use std::sync::{Weak, Arc}; use std::time::{UNIX_EPOCH, Duration}; use std::u64; -use client_traits::{EngineClient, ForceUpdateSealing}; +use client_traits::{EngineClient, ForceUpdateSealing, TransactionRequest}; use engine::{Engine, ConstructedVerifier}; use block_reward::{self, BlockRewardContract, RewardKind}; use ethjson; @@ -55,6 +55,7 @@ use engine::signer::EngineSigner; use parity_crypto::publickey::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}; @@ -72,13 +73,17 @@ use common_types::{ machine::{Call, AuxiliaryData}, }, errors::{BlockError, EthcoreError as Error, EngineError}, + ids::BlockId, snapshot::Snapshotting, + transaction::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; @@ -117,6 +122,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; @@ -168,6 +175,11 @@ impl From for AuthorityRoundParams { BlockRewardContract::new_from_address(address.into()) ); } + let randomness_contract_address = p.randomness_contract_address.map_or_else(BTreeMap::new, |transitions| { + transitions.into_iter().map(|(ethjson::uint::Uint(block), addr)| { + (block.as_u64(), addr.into()) + }).collect() + }); AuthorityRoundParams { step_durations, validators: new_validator_set(p.validators), @@ -183,6 +195,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, } } } @@ -550,6 +563,8 @@ pub struct AuthorityRound { machine: Machine, /// History of step hashes recently received from peers. received_step_hashes: RwLock>, + /// If set, enables random number contract integration. It maps the transition block to the contract address. + randomness_contract_address: BTreeMap, } // header-chain validator. @@ -851,6 +866,7 @@ impl AuthorityRound { strict_empty_steps_transition: our_params.strict_empty_steps_transition, machine, received_step_hashes: RwLock::new(Default::default()), + randomness_contract_address: our_params.randomness_contract_address, }); // Do not initialize timeouts for tests. @@ -1045,6 +1061,41 @@ impl AuthorityRound { fn address(&self) -> Option
{ self.signer.read().as_ref().map(|s| s.address() ) } + + /// Make calls to the randomness contract. + fn run_randomness_phase(&self, block: &ExecutedBlock) -> Result, Error> { + let contract_addr = match self.randomness_contract_address.range(..=block.header.number()).last() { + Some((_, &contract_addr)) => contract_addr, + None => return Ok(Vec::new()), // No randomness contract. + }; + + 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_else(|| EngineError::FailedSystemCall("Failed to upgrade to BlockchainClient.".to_string()))?; + + // Random number generation + let contract = util::BoundContract::new(&*client, BlockId::Latest, contract_addr); + let phase = randomness::RandomnessPhase::load(&contract, our_addr) + .map_err(|err| EngineError::Custom(format!("Randomness error in load(): {:?}", err)))?; + let data = match phase.advance(&contract, &mut OsRng, signer.as_ref()) + .map_err(|err| EngineError::Custom(format!("Randomness error in advance(): {:?}", err)))? { + Some(data) => data, + None => return Ok(Vec::new()), // Nothing to commit or reveal at the moment. + }; + + let nonce = block.state.nonce(&our_addr)?; + let tx_request = TransactionRequest::call(contract_addr, data).gas_price(U256::zero()).nonce(nonce); + Ok(vec![full_client.create_transaction(tx_request)?]) + } } fn unix_now() -> Duration { @@ -1326,7 +1377,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); } @@ -1430,6 +1480,10 @@ impl Engine for AuthorityRound { block_reward::apply_block_rewards(&rewards, block, &self.machine) } + fn generate_engine_transactions(&self, block: &ExecutedBlock) -> Result, Error> { + self.run_randomness_phase(block) + } + /// 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()) { @@ -1805,19 +1859,22 @@ mod tests { use std::time::Duration; use keccak_hash::keccak; use accounts::AccountProvider; + use ethabi_contract::use_contract; use ethereum_types::{Address, H520, H256, U256}; use parity_crypto::publickey::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 }, }; @@ -1831,7 +1888,7 @@ mod tests { use super::{ AuthorityRoundParams, AuthorityRound, EmptyStep, SealedEmptyStep, StepDurationInfo, - calculate_score, + calculate_score, util::BoundContract, }; fn build_aura(f: F) -> Arc where @@ -1852,6 +1909,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 @@ -2578,6 +2636,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(Some(signer)); + engine.register_client(Arc::downgrade(&client) as _); + let bc = BoundContract::new(&*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(); @@ -2734,7 +2840,7 @@ mod tests { "validators": { "list" : ["0x1000000000000000000000000000000000000001"] }, - "blockRewardContractTransition": "0", + "blockRewardContractTransition": "0", "blockRewardContractAddress": "0x2000000000000000000000000000000000000002", "blockRewardContractTransitions": { "7": "0x3000000000000000000000000000000000000003", @@ -2765,7 +2871,7 @@ mod tests { "validators": { "list" : ["0x1000000000000000000000000000000000000001"] }, - "blockRewardContractTransition": "7", + "blockRewardContractTransition": "7", "blockRewardContractAddress": "0x2000000000000000000000000000000000000002", "blockRewardContractTransitions": { "0": "0x3000000000000000000000000000000000000003", diff --git a/ethcore/engines/authority-round/src/randomness.rs b/ethcore/engines/authority-round/src/randomness.rs new file mode 100644 index 000000000..8efc45d53 --- /dev/null +++ b/ethcore/engines/authority-round/src/randomness.rs @@ -0,0 +1,257 @@ +// Copyright 2015-2019 Parity Technologies (UK) Ltd. +// This file is part of Parity Ethereum. + +// Parity Ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Ethereum. If not, see . + +//! 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. +//! +//! The process of generating random numbers is a simple finite state machine: +//! +//! ```text +//! + +//! | +//! | +//! | +//! +--------------+ +-------v-------+ +//! | | | | +//! | BeforeCommit <------------------------------+ Waiting | +//! | | enter commit phase | | +//! +------+-------+ +-------^-------+ +//! | | +//! | call | +//! | `commitHash()` | call +//! | | `revealNumber()` +//! | | +//! +------v-------+ +-------+-------+ +//! | | | | +//! | Committed +------------------------------> Reveal | +//! | | enter reveal phase | | +//! +--------------+ +---------------+ +//! ``` +//! +//! Phase transitions are performed by the smart contract and simply queried by the engine. +//! +//! Randomness generation works as follows: +//! * During the commit phase, all validators locally generate a random number, and commit that number's hash to the +//! contract. +//! * During the reveal phase, all validators reveal their local random number to the contract. The contract should +//! verify that it matches the committed hash. +//! * Finally, the XOR of all revealed numbers is used as an on-chain random number. +//! +//! An adversary can only influence that number by either controlling _all_ validators who committed, or, to a lesser +//! extent, by not revealing committed numbers. +//! The length of the commit and reveal phases, as well as any penalties for failure to reveal, are defined by the +//! contract. +//! +//! A typical case of using `RandomnessPhase` is: +//! +//! 1. `RandomnessPhase::load()` the phase from the blockchain data. +//! 2. Call `RandomnessPhase::advance()`. +//! +//! A production implementation of a randomness contract can be found here: +//! https://github.com/poanetwork/posdao-contracts/blob/4fddb108993d4962951717b49222327f3d94275b/contracts/RandomAuRa.sol + +use derive_more::Display; +use ethabi::Hash; +use ethabi_contract::use_contract; +use ethereum_types::{Address, H256, U256}; +use keccak_hash::keccak; +use log::{debug, error}; +use parity_crypto::publickey::{ecies, Error as CryptoError}; +use parity_bytes::Bytes; +use rand::Rng; +use engine::signer::EngineSigner; + +use crate::util::{BoundContract, CallError}; + +/// Random number type expected by the contract: This is generated locally, kept secret during the commit phase, and +/// published in the reveal phase. +pub type RandNumber = H256; + +use_contract!(aura_random, "../../res/contracts/authority_round_random.json"); + +/// Validated randomness phase state. +#[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, i.e. having failed to commit during the commit phase. + Waiting, + /// Indicates a commitment is possible, but still missing. + BeforeCommit, + /// 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. +/// +/// `BadRandNumber` will usually result in punishment by the contract or the other validators. +#[derive(Debug, Display)] +pub enum PhaseError { + /// 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 encrypted random number. + #[display(fmt = "Failed to load random number from the randomness contract")] + BadRandNumber, + /// Failed to encrypt random number. + #[display(fmt = "Failed to encrypt random number: {}", _0)] + Crypto(CryptoError), + /// Failed to get the engine signer's public key. + #[display(fmt = "Failed to get the engine signer's public key")] + MissingPublicKey, +} + +impl From for PhaseError { + fn from(err: CryptoError) -> PhaseError { + PhaseError::Crypto(err) + } +} + +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 random numbers). + 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_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_commit_phase { + if revealed { + return Err(PhaseError::RevealedInCommit); + } + + if !committed { + Ok(RandomnessPhase::BeforeCommit) + } 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 random seed construction process as far as possible. + /// + /// Returns the encoded contract call necessary to advance the randomness contract's state. + /// + /// **Warning**: After calling the `advance()` function, wait until the returned transaction has been included in + /// a block before calling it again; 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 => { + // Generate a new random number, but don't reveal it yet. Instead, we publish its hash to the + // randomness contract, together with the number encrypted to ourselves. That way we will later be + // able to decrypt and reveal it, and other parties are able to verify it against the hash. + let number: RandNumber = rng.gen(); + let number_hash: Hash = keccak(number.as_bytes()); + let public = signer.public().ok_or(PhaseError::MissingPublicKey)?; + let cipher = ecies::encrypt(&public, &number_hash.0, number.as_bytes())?; + + debug!(target: "engine", "Randomness contract: committing {}.", number_hash); + // Return the call data for the transaction that commits the hash and the encrypted number. + let (data, _decoder) = aura_random::functions::commit_hash::call(number_hash, cipher); + Ok(Some(data)) + } + RandomnessPhase::Reveal { round, our_address } => { + // Load the hash and encrypted number that we stored in the commit phase. + let call = aura_random::functions::get_commit_and_cipher::call(round, our_address); + let (committed_hash, cipher) = contract + .call_const(call) + .map_err(PhaseError::LoadFailed)?; + + // Decrypt the number and check against the hash. + let number_bytes = signer.decrypt(&committed_hash.0, &cipher)?; + let number = if number_bytes.len() == 32 { + RandNumber::from_slice(&number_bytes) + } 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 random number has the wrong length."); + return Err(PhaseError::BadRandNumber); + }; + let number_hash: Hash = keccak(number.as_bytes()); + if number_hash != committed_hash { + error!(target: "engine", "Decrypted random number doesn't agree with the hash."); + return Err(PhaseError::BadRandNumber); + } + + debug!(target: "engine", "Randomness contract: scheduling tx to reveal our random number {} (round={}, our_address={}).", number_hash, round, our_address); + // We are now sure that we have the correct secret and can reveal it. So we return the call data for the + // transaction that stores the revealed random bytes on the contract. + let (data, _decoder) = aura_random::functions::reveal_number::call(number.as_bytes()); + 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 000000000..8f07acbe7 --- /dev/null +++ b/ethcore/engines/authority-round/src/util.rs @@ -0,0 +1,94 @@ +// Copyright 2015-2019 Parity Technologies (UK) Ltd. +// This file is part of Parity Ethereum. + +// Parity Ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Ethereum. If not, see . + +//! 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`. + pub fn new(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: It is constant in the sense that it does not alter the EVM + /// state. + 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/contract.rs b/ethcore/engines/validator-set/src/contract.rs index e05e6c452..e235125a2 100644 --- a/ethcore/engines/validator-set/src/contract.rs +++ b/ethcore/engines/validator-set/src/contract.rs @@ -33,7 +33,7 @@ use common_types::{ engines::machine::{Call, AuxiliaryData}, }; -use client_traits::EngineClient; +use client_traits::{EngineClient, TransactionRequest}; use engine::SystemCall; use crate::{ @@ -68,7 +68,7 @@ impl ValidatorContract { match client.as_full_client() { Some(c) => { - c.transact_contract(self.contract_address, data) + c.transact(TransactionRequest::call(self.contract_address, data)) .map_err(|e| format!("Transaction import error: {}", e))?; Ok(()) }, @@ -149,7 +149,7 @@ mod tests { use accounts::AccountProvider; use call_contract::CallContract; use common_types::{header::Header, ids::BlockId}; - use client_traits::{BlockChainClient, ChainInfo, BlockInfo}; + use client_traits::{BlockChainClient, ChainInfo, BlockInfo, TransactionRequest}; use ethcore::{ miner::{self, MinerService}, test_helpers::generate_dummy_client_with_spec, @@ -225,7 +225,7 @@ mod tests { assert_eq!(client.chain_info().best_block_number, 2); // Check if misbehaving validator was removed. - client.transact_contract(Default::default(), Default::default()).unwrap(); + client.transact(TransactionRequest::call(Default::default(), Default::default())).unwrap(); client.engine().step(); client.engine().step(); assert_eq!(client.chain_info().best_block_number, 2); diff --git a/ethcore/engines/validator-set/src/multi.rs b/ethcore/engines/validator-set/src/multi.rs index 9e790cb52..1114047b4 100644 --- a/ethcore/engines/validator-set/src/multi.rs +++ b/ethcore/engines/validator-set/src/multi.rs @@ -161,11 +161,13 @@ mod tests { ids::BlockId, verification::Unverified, }; - use client_traits::{BlockChainClient, BlockInfo, ChainInfo, ImportBlock, EngineClient, ForceUpdateSealing}; + use client_traits::{ + BlockChainClient, BlockInfo, ChainInfo, ImportBlock, EngineClient, ForceUpdateSealing, TransactionRequest + }; 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 parity_crypto::publickey::Secret; @@ -190,7 +192,7 @@ mod tests { // Wrong signer for the first block. let signer = Box::new((tap.clone(), v1, "".into())); client.miner().set_author(miner::Author::Sealer(signer)); - client.transact_contract(Default::default(), Default::default()).unwrap(); + client.transact(TransactionRequest::call(Default::default(), Default::default())).unwrap(); EngineClient::update_sealing(&*client, ForceUpdateSealing::No); assert_eq!(client.chain_info().best_block_number, 0); // Right signer for the first block. @@ -199,7 +201,7 @@ mod tests { EngineClient::update_sealing(&*client, ForceUpdateSealing::No); assert_eq!(client.chain_info().best_block_number, 1); // This time v0 is wrong. - client.transact_contract(Default::default(), Default::default()).unwrap(); + client.transact(TransactionRequest::call(Default::default(), Default::default())).unwrap(); EngineClient::update_sealing(&*client, ForceUpdateSealing::No); assert_eq!(client.chain_info().best_block_number, 1); let signer = Box::new((tap.clone(), v1, "".into())); @@ -207,12 +209,12 @@ mod tests { EngineClient::update_sealing(&*client, ForceUpdateSealing::No); assert_eq!(client.chain_info().best_block_number, 2); // v1 is still good. - client.transact_contract(Default::default(), Default::default()).unwrap(); + client.transact(TransactionRequest::call(Default::default(), Default::default())).unwrap(); EngineClient::update_sealing(&*client, ForceUpdateSealing::No); 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 bcc6bba63..8c15f1054 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 parity_crypto::publickey::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 000000000..a93d1e6c1 --- /dev/null +++ b/ethcore/res/authority_round_randomness_contract.json @@ -0,0 +1,100 @@ +{ + "name": "TestAuthorityRoundRandomnessContract", + "engine": { + "authorityRound": { + "params": { + "stepDuration": 1, + "startStep": 2, + "validators": { + "list": [ + "0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e" + ] + }, + "immediateTransitions": true, + "maximumEmptySteps": "2", + "randomnessContractAddress": { + "0": "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", + "pricing": { + "0x0": { + "price": { "linear": { "base": 500, "word": 0 }} + }, + "0x7fffffffffffff": { + "price": { "linear": { "base": 150, "word": 0 }} + } + } + } + }, + "0000000000000000000000000000000000000007": { + "balance": "1", + "builtin": { + "name": "alt_bn128_mul", + "pricing": { + "0x0": { + "price": { "linear": { "base": 40000, "word": 0 }} + }, + "0x7fffffffffffff": { + "price": { "linear": { "base": 6000, "word": 0 }} + } + } + } + }, + "0000000000000000000000000000000000000008": { + "balance": "1", + "builtin": { + "name": "alt_bn128_pairing", + "pricing": { + "0x0": { + "price": { "alt_bn128_pairing": { "base": 100000, "pair": 80000 }} + }, + "0x7fffffffffffff": { + "price": { "alt_bn128_pairing": { "base": 45000, "pair": 34000 }} + } + } + } + }, + "0000000000000000000000000000000000000042": { + "balance": "1", + "constructor": "608060405234801561001057600080fd5b50610820806100206000396000f3fe608060405234801561001057600080fd5b50600436106100ec576000357c01000000000000000000000000000000000000000000000000000000009004806363f160e6116100a95780637a3e286b116100835780637a3e286b14610378578063baf11cab14610380578063c358ced0146103ac578063fe7d567d146103b4576100ec565b806363f160e614610285578063695e89f6146102c557806374ce906714610370576100ec565b806304fdb016146100f15780630b61ba8514610192578063209652551461020b5780632e8a8dd5146102255780633fa4f245146102515780635580e58b14610259575b600080fd5b61011d6004803603604081101561010757600080fd5b5080359060200135600160a060020a03166103d1565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561015757818101518382015260200161013f565b50505050905090810190601f1680156101845780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b610209600480360360408110156101a857600080fd5b813591908101906040810160208201356401000000008111156101ca57600080fd5b8201836020820111156101dc57600080fd5b803590602001918460018302840111640100000000831117156101fe57600080fd5b509092509050610475565b005b6102136104fa565b60408051918252519081900360200190f35b6102136004803603604081101561023b57600080fd5b5080359060200135600160a060020a0316610501565b61021361051b565b6102136004803603604081101561026f57600080fd5b5080359060200135600160a060020a0316610521565b6102b16004803603604081101561029b57600080fd5b5080359060200135600160a060020a031661053e565b604080519115158252519081900360200190f35b6102f1600480360360408110156102db57600080fd5b5080359060200135600160a060020a0316610568565b6040518083815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561033457818101518382015260200161031c565b50505050905090810190601f1680156103615780820380516001836020036101000a031916815260200191505b50935050505060405180910390f35b6102b1610639565b610213610649565b6102b16004803603604081101561039657600080fd5b5080359060200135600160a060020a0316610654565b6102b161067c565b610209600480360360208110156103ca57600080fd5b5035610687565b600160208181526000938452604080852082529284529282902080548351600293821615610100026000190190911692909204601f8101859004850283018501909352828252909290919083018282801561046d5780601f106104425761010080835404028352916020019161046d565b820191906000526020600020905b81548152906001019060200180831161045057829003601f168201915b505050505081565b41331461048157600080fd5b61048d60014303610735565b61049657600080fd5b60006104a460014303610740565b90506104b08133610654565b156104ba57600080fd5b600081815260208181526040808320338085529083528184208890558484526001835281842090845290915290206104f3908484610753565b5050505050565b6003545b90565b600060208181529281526040808220909352908152205481565b60035481565b600260209081526000928352604080842090915290825290205481565b6000918252600260209081526040808420600160a060020a03939093168452919052902054151590565b600082815260208181526040808320600160a060020a03851680855290835281842054868552600180855283862092865291845282852080548451600294821615610100026000190190911693909304601f810186900486028401860190945283835260609491939092918391908301828280156106275780601f106105fc57610100808354040283529160200191610627565b820191906000526020600020905b81548152906001019060200180831161060a57829003601f168201915b50505050509050915091509250929050565b600061064443610735565b905090565b600061064443610740565b600091825260208281526040808420600160a060020a03939093168452919052902054151590565b600061064443610747565b41331461069357600080fd5b61069f60014303610747565b6106a857600080fd5b60006106b660014303610740565b90506106c2813361053e565b156106cc57600080fd5b60408051602080820185905282518083038201815291830183528151918101919091206000848152808352838120338252909252919020541461070e57600080fd5b60009081526002602090815260408083203384529091529020819055600380549091189055565b600360069091061090565b6006900490565b60036006909106101590565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106107945782800160ff198235161785556107c1565b828001600101855582156107c1579182015b828111156107c15782358255916020019190600101906107a6565b506107cd9291506107d1565b5090565b6104fe91905b808211156107cd57600081556001016107d756fea265627a7a7230582008bb7311af9026bd70ddb998741333d414a366275b9b433a2943bbd6bedc27ae64736f6c634300050a0032" + } + } +} diff --git a/ethcore/res/contracts/authority_round_random.json b/ethcore/res/contracts/authority_round_random.json new file mode 100644 index 000000000..2ac5440c8 --- /dev/null +++ b/ethcore/res/contracts/authority_round_random.json @@ -0,0 +1,133 @@ +[{ + "constant": false, + "inputs": [{ + "name": "_secretHash", + "type": "bytes32" + }, + { + "name": "_cipher", + "type": "bytes" + } + ], + "name": "commitHash", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [{ + "name": "_number", + "type": "uint256" + }], + "name": "revealNumber", + "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": "_miningAddress", + "type": "address" + } + ], + "name": "getCommitAndCipher", + "outputs": [ + { + "name": "", + "type": "bytes32" + }, + { + "name": "", + "type": "bytes" + } + ], + "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 000000000..396198605 --- /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": "_number", + "type": "uint256" + } + ], + "name": "revealNumber", + "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 000000000..e1dfde013 --- /dev/null +++ b/ethcore/res/contracts/test_authority_round_random.sol @@ -0,0 +1,102 @@ +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 _number The validator's secret. + function revealNumber(uint256 _number) 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(_number))); + secrets[round][msg.sender] = _number; + value ^= _number; + } + + /// @dev Returns the Keccak-256 hash and 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 hash and cipher should be retrieved. + /// @param _miningAddress The mining address of validator. + function getCommitAndCipher( + uint256 _collectRound, + address _miningAddress + ) public view returns(bytes32, bytes memory) { + return (hashes[_collectRound][_miningAddress], ciphers[_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 hashes[_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/snapshot/snapshot-tests/src/service.rs b/ethcore/snapshot/snapshot-tests/src/service.rs index 9b85f324f..003aeee90 100644 --- a/ethcore/snapshot/snapshot-tests/src/service.rs +++ b/ethcore/snapshot/snapshot-tests/src/service.rs @@ -50,7 +50,7 @@ use journaldb::Algorithm; #[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(); @@ -143,7 +143,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"); @@ -207,7 +207,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(); @@ -266,7 +266,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(); @@ -374,7 +374,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/spec/src/chain.rs b/ethcore/spec/src/chain.rs index 5d0950914..bb171abd5 100644 --- a/ethcore/spec/src/chain.rs +++ b/ethcore/spec/src/chain.rs @@ -86,6 +86,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 5db0a193a..eb3d6386f 100644 --- a/ethcore/src/client/client.rs +++ b/ethcore/src/client/client.rs @@ -79,6 +79,7 @@ use client_traits::{ StateOrBlock, Tick, TransactionInfo, + TransactionRequest, ForceUpdateSealing }; use db::{keys::BlockDetails, Readable, Writable}; @@ -2154,29 +2155,35 @@ impl BlockChainClient for Client { } } - fn transact_contract(&self, address: Address, data: Bytes) -> Result<(), transaction::Error> { + fn create_transaction(&self, TransactionRequest { action, data, gas, gas_price, nonce }: TransactionRequest) + -> 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, tx_request: TransactionRequest) -> Result<(), transaction::Error> { + let signed = self.create_transaction(tx_request)?; self.importer.miner.import_own_transaction(self, signed.into()) } } @@ -2978,7 +2985,7 @@ mod tests { #[test] fn should_mark_finalization_correctly_for_parent() { - let client = generate_dummy_client_with_spec_and_data(spec::new_test_with_finality, 2, 0, &[]); + let client = generate_dummy_client_with_spec_and_data(spec::new_test_with_finality, 2, 0, &[], false); let chain = client.chain(); let block1_details = chain.block_hash(1).and_then(|h| chain.block_details(&h)); diff --git a/ethcore/src/miner/miner.rs b/ethcore/src/miner/miner.rs index e3d3e0442..755e7ba89 100644 --- a/ethcore/src/miner/miner.rs +++ b/ethcore/src/miner/miner.rs @@ -327,6 +327,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 { @@ -336,6 +343,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()) } @@ -422,7 +430,8 @@ impl Miner { let chain_info = chain.chain_info(); // Open block - let (mut open_block, original_work_hash) = { + // Some engines add transactions to the block for their own purposes, e.g. AuthorityRound RANDAO. + let (mut open_block, original_work_hash, engine_txs) = { 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; @@ -433,38 +442,48 @@ 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"); // add transactions to old_block - chain.reopen_block(old_block) + (chain.reopen_block(old_block), last_work_hash, Vec::new()) } 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( + let 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. + match self.engine.generate_engine_transactions(&block) { + Ok(transactions) => (block, last_work_hash, 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) }; + if self.options.infinite_pending_block { + open_block.remove_gas_limit(); + } + let mut invalid_transactions = HashSet::new(); let mut not_allowed_transactions = HashSet::new(); let mut senders_to_penalize = HashSet::new(); @@ -488,13 +507,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_txs: 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_txs.len()), ordering: miner::PendingOrdering::Priority, } ); @@ -504,12 +523,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_txs.len() + queue_txs.len()); - for tx in pending { + for transaction in engine_txs.into_iter().chain(queue_txs.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/test_helpers/mod.rs b/ethcore/src/test_helpers/mod.rs index 4c427afcd..c5b064d52 100644 --- a/ethcore/src/test_helpers/mod.rs +++ b/ethcore/src/test_helpers/mod.rs @@ -118,31 +118,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/src/test_helpers/test_client.rs b/ethcore/src/test_helpers/test_client.rs index 403c64c0e..41119bde2 100644 --- a/ethcore/src/test_helpers/test_client.rs +++ b/ethcore/src/test_helpers/test_client.rs @@ -72,7 +72,7 @@ use client::{ use client_traits::{ BlockInfo, Nonce, Balance, ChainInfo, TransactionInfo, BlockChainClient, ImportBlock, AccountData, BlockChain, IoClient, BadBlocks, ScheduleInfo, StateClient, ProvingBlockChainClient, - StateOrBlock, ForceUpdateSealing + StateOrBlock, ForceUpdateSealing, TransactionRequest }; use engine::Engine; use machine::executed::Executed; @@ -912,18 +912,24 @@ impl BlockChainClient for TestBlockChainClient { } } - fn transact_contract(&self, address: Address, data: Bytes) -> Result<(), transaction::Error> { + fn create_transaction(&self, TransactionRequest { action, data, gas, gas_price, nonce }: TransactionRequest) + -> 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, tx_request: TransactionRequest) -> Result<(), transaction::Error> { + let signed = self.create_transaction(tx_request)?; self.miner.import_own_transaction(self, signed.into()) } } diff --git a/ethcore/types/src/errors/engine_error.rs b/ethcore/types/src/errors/engine_error.rs index 02cdd066b..d5168875e 100644 --- a/ethcore/types/src/errors/engine_error.rs +++ b/ethcore/types/src/errors/engine_error.rs @@ -37,6 +37,10 @@ pub enum EngineError { InsufficientProof(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. @@ -91,6 +95,8 @@ impl fmt::Display for EngineError { BadSealFieldSize(ref oob) => format!("Seal field has an unexpected length: {}", oob), InsufficientProof(ref msg) => format!("Insufficient validation proof: {}", msg), 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 ba5ad63b3..69780009d 100644 --- a/json/src/spec/authority_round.rs +++ b/json/src/spec/authority_round.rs @@ -95,6 +95,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. @@ -124,7 +126,11 @@ mod tests { "validateStepTransition": 150, "blockReward": 5000000, "maximumUncleCountTransition": 10000000, - "maximumUncleCount": 5 + "maximumUncleCount": 5, + "randomnessContractAddress": { + "10": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "20": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } } }"#; @@ -138,6 +144,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(), + 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 c6b86a01a..a6f90f6a7 100644 --- a/rpc/src/v1/helpers/engine_signer.rs +++ b/rpc/src/v1/helpers/engine_signer.rs @@ -18,7 +18,7 @@ use std::sync::Arc; use accounts::AccountProvider; use ethkey::Password; -use crypto::publickey::{Address, Message, Signature, Error}; +use crypto::publickey::{Address, Message, Public, Signature, Error}; /// An implementation of EngineSigner using internal account management. pub struct EngineSigner { @@ -42,8 +42,19 @@ impl engine::signer::EngineSigner for EngineSigner { } } + fn decrypt(&self, auth_data: &[u8], cipher: &[u8]) -> Result, Error> { + self.accounts.decrypt(self.address, None, auth_data, cipher).map_err(|e| { + warn!("Unable to decrypt message: {:?}", e); + Error::InvalidMessage + }) + } + fn address(&self) -> Address { self.address } + + fn public(&self) -> Option { + self.accounts.account_public(self.address, &self.password).ok() + } }