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 <dvdplm@gmail.com>

* 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.
This commit is contained in:
Andreas Fackler 2019-12-17 11:34:14 +01:00 committed by David
parent f6909d8243
commit 2b1d148ceb
26 changed files with 1293 additions and 69 deletions

6
Cargo.lock generated
View File

@ -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",

View File

@ -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"] }

View File

@ -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<SignedTransaction, transaction::Error>;
/// 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<U256>,
pub gas_price: Option<U256>,
pub nonce: Option<U256>,
}
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

View File

@ -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<Vec<SignedTransaction>, Error> {
Ok(Vec::new())
}
/// Returns the engine's current sealing state.
fn sealing_state(&self) -> SealingState { SealingState::External }

View File

@ -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<Vec<u8>, Error>;
/// The signer's public key, if available.
fn public(&self) -> Option<Public>;
}
/// 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<Vec<u8>, Error> {
ecies::decrypt(self.0.secret(), auth_data, cipher)
}
fn address(&self) -> Address {
self.0.address()
}
fn public(&self) -> Option<Public> {
Some(*self.0.public())
}
}

View File

@ -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<AccountProvider>, Address, Password) {
}
}
fn decrypt(&self, auth_data: &[u8], cipher: &[u8]) -> Result<Vec<u8>, 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<Public> {
self.0.account_public(self.1, &self.2).ok()
}
}

View File

@ -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" }

View File

@ -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<u64, Address>,
}
const U16_MAX: usize = ::std::u16::MAX as usize;
@ -168,6 +175,11 @@ impl From<ethjson::spec::AuthorityRoundParams> 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<ethjson::spec::AuthorityRoundParams> 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<BTreeMap<(u64, Address), H256>>,
/// If set, enables random number contract integration. It maps the transition block to the contract address.
randomness_contract_address: BTreeMap<u64, Address>,
}
// 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<Address> {
self.signer.read().as_ref().map(|s| s.address() )
}
/// Make calls to the randomness contract.
fn run_randomness_phase(&self, block: &ExecutedBlock) -> Result<Vec<SignedTransaction>, 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<Vec<SignedTransaction>, 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: F) -> Arc<AuthorityRound> 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",

View File

@ -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 <http://www.gnu.org/licenses/>.
//! 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<CryptoError> 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<RandomnessPhase, PhaseError> {
// 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<R: Rng>(
self,
contract: &BoundContract,
rng: &mut R,
signer: &dyn EngineSigner,
) -> Result<Option<Bytes>, 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))
}
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//! 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<D>(&self, call: (ethabi::Bytes, D)) -> Result<D::Output, CallError>
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)
}
}

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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"
}
}
}

View File

@ -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"
}
]

View File

@ -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"
}
]

View File

@ -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;
}
}

View File

@ -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::<ClientIoMessage<Client>>::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();

View File

@ -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,

View File

@ -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<SignedTransaction, transaction::Error>
{
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));

View File

@ -327,6 +327,13 @@ impl Miner {
///
/// NOTE This should be only used for tests.
pub fn new_for_tests(spec: &Spec, accounts: Option<HashSet<Address>>) -> 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<HashSet<Address>>, 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<Arc<_>> = self.transaction_queue.pending(
let queue_txs: Vec<Arc<_>> = 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();

View File

@ -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<Client> {
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<Client> {
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<F>(test_spec: F) -> Arc<Client> 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<F>(test_spec: F, block_number: u32, txs_per_block: usize, tx_gas_prices: &[U256]) -> Arc<Client> where
pub fn generate_dummy_client_with_spec_and_data<F>(
test_spec: F, block_number: u32, txs_per_block: usize, tx_gas_prices: &[U256], force_sealing: bool,
) -> Arc<Client> 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;

View File

@ -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<SignedTransaction, transaction::Error>
{
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())
}
}

View File

@ -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"),

View File

@ -95,6 +95,8 @@ pub struct AuthorityRoundParams {
pub strict_empty_steps_transition: Option<Uint>,
/// First block for which a 2/3 quorum (instead of 1/2) is required.
pub two_thirds_majority_transition: Option<Uint>,
/// The random number contract's address, or a map of contract transitions.
pub randomness_contract_address: Option<BTreeMap<Uint, Address>>,
}
/// 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());
}
}

View File

@ -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<Vec<u8>, 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<Public> {
self.accounts.account_public(self.address, &self.password).ok()
}
}