openethereum/crates/ethcore/src/engines/authority_round/randomness.rs

258 lines
12 KiB
Rust

// Copyright 2015-2020 Parity Technologies (UK) Ltd.
// This file is part of OpenEthereum.
// OpenEthereum 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.
// OpenEthereum 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 OpenEthereum. 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 bytes::Bytes;
use crypto::publickey::{ecies, Error as CryptoError};
use derive_more::Display;
use engines::signer::EngineSigner;
use ethabi::Hash;
use ethabi_contract::use_contract;
use ethereum_types::{Address, H256, U256};
use hash::keccak;
use log::{debug, error};
use rand::Rng;
use super::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.0);
let public = signer.public().ok_or(PhaseError::MissingPublicKey)?;
let cipher = ecies::encrypt(&public, number_hash.as_bytes(), 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.0);
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.0);
Ok(Some(data))
}
}
}
}