Basic Authority (#991)

* Firt commit.

* First non-functional but correct implementation of BasicAuthority.

Still needs:
- Sealing infrastructure.

* Punch a hole to give miner access to key store.

* Fix test built.

* Basic version of synchronous mining.

This will seal a block whenever a new transaction comes through.
To be made better we need a timer which will wait for one second after the
last block before sealing a new one - better still would be to cooperatively
interleave blocks with other sealing nodes.

* Add tests.

* Fix minor issues from repotting.

* Address grumbles.
This commit is contained in:
Gav Wood
2016-05-03 17:23:53 +02:00
parent 1583f7d434
commit ac73b2628a
34 changed files with 506 additions and 88 deletions

View File

@@ -0,0 +1,297 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity 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 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. If not, see <http://www.gnu.org/licenses/>.
//! A blockchain engine that supports a basic, non-BFT proof-of-authority.
use common::*;
use util::keys::store::AccountProvider;
use block::*;
use spec::{CommonParams, Spec};
use engine::*;
use evm::{Schedule, Factory};
use ethjson;
/// BasicAuthority params.
#[derive(Debug, PartialEq)]
pub struct BasicAuthorityParams {
/// Gas limit divisor.
pub gas_limit_bound_divisor: U256,
/// Block duration.
pub duration_limit: u64,
/// Valid signatories.
pub authorities: HashSet<Address>,
}
impl From<ethjson::spec::BasicAuthorityParams> for BasicAuthorityParams {
fn from(p: ethjson::spec::BasicAuthorityParams) -> Self {
BasicAuthorityParams {
gas_limit_bound_divisor: p.gas_limit_bound_divisor.into(),
duration_limit: p.duration_limit.into(),
authorities: p.authorities.into_iter().map(Into::into).collect::<HashSet<_>>(),
}
}
}
/// Engine using BasicAuthority proof-of-work consensus algorithm, suitable for Ethereum
/// mainnet chains in the Olympic, Frontier and Homestead eras.
pub struct BasicAuthority {
params: CommonParams,
our_params: BasicAuthorityParams,
builtins: BTreeMap<Address, Builtin>,
factory: Factory,
}
impl BasicAuthority {
/// Create a new instance of BasicAuthority engine
pub fn new(params: CommonParams, our_params: BasicAuthorityParams, builtins: BTreeMap<Address, Builtin>) -> Self {
BasicAuthority {
params: params,
our_params: our_params,
builtins: builtins,
factory: Factory::default(),
}
}
}
impl Engine for BasicAuthority {
fn name(&self) -> &str { "BasicAuthority" }
fn version(&self) -> SemanticVersion { SemanticVersion::new(1, 0, 0) }
// One field - the signature
fn seal_fields(&self) -> usize { 1 }
fn params(&self) -> &CommonParams { &self.params }
fn builtins(&self) -> &BTreeMap<Address, Builtin> { &self.builtins }
/// Additional engine-specific information for the user/developer concerning `header`.
fn extra_info(&self, _header: &Header) -> HashMap<String, String> { hash_map!["signature".to_owned() => "TODO".to_owned()] }
fn vm_factory(&self) -> &Factory { &self.factory }
fn schedule(&self, _env_info: &EnvInfo) -> Schedule {
Schedule::new_homestead()
}
fn populate_from_parent(&self, header: &mut Header, parent: &Header, gas_floor_target: U256) {
header.difficulty = parent.difficulty;
header.gas_limit = {
let gas_limit = parent.gas_limit;
let bound_divisor = self.our_params.gas_limit_bound_divisor;
if gas_limit < gas_floor_target {
min(gas_floor_target, gas_limit + gas_limit / bound_divisor - x!(1))
} else {
max(gas_floor_target, gas_limit - gas_limit / bound_divisor + x!(1))
}
};
header.note_dirty();
// info!("ethash: populate_from_parent #{}: difficulty={} and gas_limit={}", header.number, header.difficulty, header.gas_limit);
}
/// Apply the block reward on finalisation of the block.
/// This assumes that all uncles are valid uncles (i.e. of at least one generation before the current).
fn on_close_block(&self, _block: &mut ExecutedBlock) {}
/// Attempt to seal the block internally.
///
/// This operation is synchronous and may (quite reasonably) not be available, in which `false` will
/// be returned.
fn generate_seal(&self, block: &ExecutedBlock, accounts: Option<&AccountProvider>) -> Option<Vec<Bytes>> {
if let Some(ap) = accounts {
// check to see if author is contained in self.our_params.authorities
if self.our_params.authorities.contains(block.header().author()) {
if let Ok(secret) = ap.account_secret(block.header().author()) {
return Some(block.header().author_seal(&secret));
} else {
trace!(target: "basicauthority", "generate_seal: FAIL: accounts secret key unavailable");
}
} else {
trace!(target: "basicauthority", "generate_seal: FAIL: block author {} isn't one of the authorized accounts {:?}", block.header().author(), self.our_params.authorities);
}
} else {
trace!(target: "basicauthority", "generate_seal: FAIL: accounts not provided");
}
None
}
fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> {
// check the seal fields.
// TODO: pull this out into common code.
if header.seal.len() != self.seal_fields() {
return Err(From::from(BlockError::InvalidSealArity(
Mismatch { expected: self.seal_fields(), found: header.seal.len() }
)));
}
Ok(())
}
fn verify_block_unordered(&self, header: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> {
// check the signature is legit.
let sig = try!(UntrustedRlp::new(&header.seal[0]).as_val::<H520>());
let signer = Address::from(try!(ec::recover(&sig, &header.bare_hash())).sha3());
if !self.our_params.authorities.contains(&signer) {
return try!(Err(BlockError::InvalidSeal));
}
Ok(())
}
fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> {
// we should not calculate difficulty for genesis blocks
if header.number() == 0 {
return Err(From::from(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() })));
}
// Check difficulty is correct given the two timestamps.
if header.difficulty() != parent.difficulty() {
return Err(From::from(BlockError::InvalidDifficulty(Mismatch { expected: *parent.difficulty(), found: *header.difficulty() })))
}
let gas_limit_divisor = self.our_params.gas_limit_bound_divisor;
let min_gas = parent.gas_limit - parent.gas_limit / gas_limit_divisor;
let max_gas = parent.gas_limit + parent.gas_limit / gas_limit_divisor;
if header.gas_limit <= min_gas || header.gas_limit >= max_gas {
return Err(From::from(BlockError::InvalidGasLimit(OutOfBounds { min: Some(min_gas), max: Some(max_gas), found: header.gas_limit })));
}
Ok(())
}
fn verify_transaction_basic(&self, t: &SignedTransaction, _header: &Header) -> result::Result<(), Error> {
try!(t.check_low_s());
Ok(())
}
fn verify_transaction(&self, t: &SignedTransaction, _header: &Header) -> Result<(), Error> {
t.sender().map(|_|()) // Perform EC recovery and cache sender
}
}
impl Header {
/// Get the none field of the header.
pub fn signature(&self) -> H520 {
decode(&self.seal()[0])
}
/// Generate a seal for the block with the given `secret`.
pub fn author_seal(&self, secret: &Secret) -> Vec<Bytes> {
vec![encode(&ec::sign(secret, &self.bare_hash()).unwrap_or(Signature::new())).to_vec()]
}
/// Set the nonce and mix hash fields of the header.
pub fn sign(&mut self, secret: &Secret) {
self.seal = self.author_seal(secret);
}
}
/// Create a new test chain spec with BasicAuthority consensus engine.
pub fn new_test_authority() -> Spec { Spec::load(include_bytes!("../res/test_authority.json")) }
#[cfg(test)]
mod tests {
use super::*;
use common::*;
use block::*;
use engine::*;
use tests::helpers::*;
use util::keys::{TestAccountProvider, TestAccount};
#[test]
fn has_valid_metadata() {
let engine = new_test_authority().engine;
assert!(!engine.name().is_empty());
assert!(engine.version().major >= 1);
}
#[test]
fn can_return_factory() {
let engine = new_test_authority().engine;
engine.vm_factory();
}
#[test]
fn can_return_schedule() {
let engine = new_test_authority().engine;
let schedule = engine.schedule(&EnvInfo {
number: 10000000,
author: x!(0),
timestamp: 0,
difficulty: x!(0),
last_hashes: vec![],
gas_used: x!(0),
gas_limit: x!(0)
});
assert!(schedule.stack_limit > 0);
}
#[test]
fn can_do_seal_verification_fail() {
let engine = new_test_authority().engine;
let header: Header = Header::default();
let verify_result = engine.verify_block_basic(&header, None);
match verify_result {
Err(Error::Block(BlockError::InvalidSealArity(_))) => {},
Err(_) => { panic!("should be block seal-arity mismatch error (got {:?})", verify_result); },
_ => { panic!("Should be error, got Ok"); },
}
}
#[test]
fn can_do_signature_verification_fail() {
let engine = new_test_authority().engine;
let mut header: Header = Header::default();
header.set_seal(vec![rlp::encode(&Signature::zero()).to_vec()]);
let verify_result = engine.verify_block_unordered(&header, None);
match verify_result {
Err(Error::Util(UtilError::Crypto(CryptoError::InvalidSignature))) => {},
Err(_) => { panic!("should be block difficulty error (got {:?})", verify_result); },
_ => { panic!("Should be error, got Ok"); },
}
}
#[test]
fn can_do_signature_verification() {
let secret = "".sha3();
let addr = KeyPair::from_secret("".sha3()).unwrap().address();
let engine = new_test_authority().engine;
let mut header: Header = Header::default();
header.set_author(addr);
header.sign(&secret);
assert!(engine.verify_block_unordered(&header, None).is_ok());
}
#[test]
fn can_generate_seal() {
let addr = KeyPair::from_secret("".sha3()).unwrap().address();
let accounts = hash_map![addr => TestAccount{unlocked: true, password: Default::default(), secret: "".sha3()}];
let tap = TestAccountProvider::new(accounts);
let spec = new_test_authority();
let engine = &spec.engine;
let genesis_header = spec.genesis_header();
let mut db_result = get_temp_journal_db();
let mut db = db_result.take();
spec.ensure_db_good(db.as_hashdb_mut());
let last_hashes = vec![genesis_header.hash()];
let b = OpenBlock::new(engine.deref(), false, db, &genesis_header, last_hashes, addr.clone(), x!(3141562), vec![]);
let b = b.close_and_lock();
let seal = engine.generate_seal(b.block(), Some(&tap)).unwrap();
assert!(b.try_seal(engine.deref(), seal).is_ok());
}
}

View File

@@ -89,7 +89,7 @@ pub struct ExecutedBlock {
/// A set of references to `ExecutedBlock` fields that are publicly accessible.
pub struct BlockRefMut<'a> {
/// Block header.
pub header: &'a Header,
pub header: &'a mut Header,
/// Block transactions.
pub transactions: &'a Vec<SignedTransaction>,
/// Block uncles.
@@ -133,7 +133,7 @@ impl ExecutedBlock {
/// Get a structure containing individual references to all public fields.
pub fn fields_mut(&mut self) -> BlockRefMut {
BlockRefMut {
header: &self.base.header,
header: &mut self.base.header,
transactions: &self.base.transactions,
uncles: &self.base.uncles,
state: &mut self.state,

View File

@@ -42,8 +42,7 @@ use env_info::EnvInfo;
use executive::{Executive, Executed, TransactOptions, contract_address};
use receipt::LocalizedReceipt;
pub use blockchain::CacheSize as BlockChainCacheSize;
use trace::{TraceDB, ImportRequest as TraceImportRequest, LocalizedTrace, Database as TraceDatabase, Filter as
TracedbFilter};
use trace::{TraceDB, ImportRequest as TraceImportRequest, LocalizedTrace, Database as TraceDatabase, Filter as TracedbFilter};
use trace;
/// General block status

View File

@@ -34,9 +34,10 @@ use std::collections::HashSet;
use util::bytes::Bytes;
use util::hash::{Address, H256, H2048};
use util::numbers::U256;
use util::keys::store::AccountProvider;
use blockchain::TreeRoute;
use block_queue::BlockQueueInfo;
use block::{ClosedBlock, LockedBlock, SealedBlock};
use block::{ExecutedBlock, ClosedBlock, LockedBlock, SealedBlock};
use header::{BlockNumber, Header};
use transaction::{LocalizedTransaction, SignedTransaction};
use log_entry::LocalizedLogEntry;
@@ -134,6 +135,9 @@ pub trait BlockChainClient : Sync + Send {
/// Makes a non-persistent transaction call.
fn call(&self, t: &SignedTransaction) -> Result<Executed, Error>;
/// Attempt to seal the block internally. See `Engine`.
fn generate_seal(&self, block: &ExecutedBlock, accounts: Option<&AccountProvider>) -> Option<Vec<Bytes>> { self.engine().generate_seal(block, accounts) }
/// Executes a function providing it with a reference to an engine.
fn engine(&self) -> &Engine;

View File

@@ -15,10 +15,10 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use common::*;
use util::keys::store::AccountProvider;
use block::ExecutedBlock;
use spec::CommonParams;
use evm::Schedule;
use evm::Factory;
use evm::{Schedule, Factory};
/// A consensus mechanism for the chain. Generally either proof-of-work or proof-of-stake-based.
/// Provides hooks into each of the major parts of block import.
@@ -61,6 +61,14 @@ pub trait Engine : Sync + Send {
/// Block transformation functions, after the transactions.
fn on_close_block(&self, _block: &mut ExecutedBlock) {}
/// Attempt to seal the block internally.
///
/// If `Some` is returned, then you get a valid seal.
///
/// This operation is synchronous and may (quite reasonably) not be available, in which None will
/// be returned.
fn generate_seal(&self, _block: &ExecutedBlock, _accounts: Option<&AccountProvider>) -> Option<Vec<Bytes>> { None }
/// Phase 1 quick block verification. Only does checks that are cheap. `block` (the header's full block)
/// may be provided for additional checks. Returns either a null `Ok` or a general error detailing the problem with import.
fn verify_block_basic(&self, _header: &Header, _block: Option<&[u8]>) -> Result<(), Error> { Ok(()) }

View File

@@ -137,6 +137,8 @@ pub enum BlockError {
MismatchedH256SealElement(Mismatch<H256>),
/// Proof-of-work aspect of seal, which we assume is a 256-bit value, is invalid.
InvalidProofOfWork(OutOfBounds<U256>),
/// Some low-level aspect of the seal in incorrect.
InvalidSeal,
/// Gas limit header field is invalid.
InvalidGasLimit(OutOfBounds<U256>),
/// Receipts trie root header field is invalid.

View File

@@ -27,8 +27,6 @@ use ethjson;
/// Ethash params.
#[derive(Debug, PartialEq)]
pub struct EthashParams {
/// Tie breaking gas.
pub tie_breaking_gas: bool,
/// Gas limit divisor.
pub gas_limit_bound_divisor: U256,
/// Minimum difficulty.
@@ -41,18 +39,20 @@ pub struct EthashParams {
pub block_reward: U256,
/// Namereg contract address.
pub registrar: Address,
/// Homestead transition block number.
pub frontier_compatibility_mode_limit: u64,
}
impl From<ethjson::spec::EthashParams> for EthashParams {
fn from(p: ethjson::spec::EthashParams) -> Self {
EthashParams {
tie_breaking_gas: p.tie_breaking_gas,
gas_limit_bound_divisor: p.gas_limit_bound_divisor.into(),
minimum_difficulty: p.minimum_difficulty.into(),
difficulty_bound_divisor: p.difficulty_bound_divisor.into(),
duration_limit: p.duration_limit.into(),
block_reward: p.block_reward.into(),
registrar: p.registrar.into(),
frontier_compatibility_mode_limit: p.frontier_compatibility_mode_limit.into(),
}
}
}
@@ -100,9 +100,9 @@ impl Engine for Ethash {
}
fn schedule(&self, env_info: &EnvInfo) -> Schedule {
trace!(target: "client", "Creating schedule. fCML={}", self.params.frontier_compatibility_mode_limit);
trace!(target: "client", "Creating schedule. fCML={}", self.ethash_params.frontier_compatibility_mode_limit);
if env_info.number < self.params.frontier_compatibility_mode_limit {
if env_info.number < self.ethash_params.frontier_compatibility_mode_limit {
Schedule::new_frontier()
} else {
Schedule::new_homestead()
@@ -207,7 +207,7 @@ impl Engine for Ethash {
}
fn verify_transaction_basic(&self, t: &SignedTransaction, header: &Header) -> result::Result<(), Error> {
if header.number() >= self.params.frontier_compatibility_mode_limit {
if header.number() >= self.ethash_params.frontier_compatibility_mode_limit {
try!(t.check_low_s());
}
Ok(())
@@ -229,7 +229,7 @@ impl Ethash {
let min_difficulty = self.ethash_params.minimum_difficulty;
let difficulty_bound_divisor = self.ethash_params.difficulty_bound_divisor;
let duration_limit = self.ethash_params.duration_limit;
let frontier_limit = self.params.frontier_compatibility_mode_limit;
let frontier_limit = self.ethash_params.frontier_compatibility_mode_limit;
let mut target = if header.number < frontier_limit {
if header.timestamp >= parent.timestamp + duration_limit {

View File

@@ -89,6 +89,7 @@ extern crate bloomchain;
#[cfg(test)] extern crate ethcore_devtools as devtools;
#[cfg(feature = "jit" )] extern crate evmjit;
pub mod basic_authority;
pub mod block;
pub mod block_queue;
pub mod client;

View File

@@ -57,11 +57,7 @@ impl Engine for NullEngine {
&self.builtins
}
fn schedule(&self, env_info: &EnvInfo) -> Schedule {
if env_info.number < self.params.frontier_compatibility_mode_limit {
Schedule::new_frontier()
} else {
Schedule::new_homestead()
}
fn schedule(&self, _env_info: &EnvInfo) -> Schedule {
Schedule::new_homestead()
}
}

View File

@@ -24,6 +24,7 @@ use account_db::*;
use super::genesis::Genesis;
use super::seal::Generic as GenericSeal;
use ethereum;
use basic_authority::BasicAuthority;
use ethjson;
/// Parameters common to all engines.
@@ -31,8 +32,6 @@ use ethjson;
pub struct CommonParams {
/// Account start nonce.
pub account_start_nonce: U256,
/// Frontier compatibility mode limit.
pub frontier_compatibility_mode_limit: u64,
/// Maximum size of extra data.
pub maximum_extra_data_size: usize,
/// Network id.
@@ -45,7 +44,6 @@ impl From<ethjson::spec::Params> for CommonParams {
fn from(p: ethjson::spec::Params) -> Self {
CommonParams {
account_start_nonce: p.account_start_nonce.into(),
frontier_compatibility_mode_limit: p.frontier_compatibility_mode_limit.into(),
maximum_extra_data_size: p.maximum_extra_data_size.into(),
network_id: p.network_id.into(),
min_gas_limit: p.min_gas_limit.into(),
@@ -131,7 +129,8 @@ impl Spec {
fn engine(engine_spec: ethjson::spec::Engine, params: CommonParams, builtins: BTreeMap<Address, Builtin>) -> Box<Engine> {
match engine_spec {
ethjson::spec::Engine::Null => Box::new(NullEngine::new(params, builtins)),
ethjson::spec::Engine::Ethash(ethash) => Box::new(ethereum::Ethash::new(params, From::from(ethash.params), builtins))
ethjson::spec::Engine::Ethash(ethash) => Box::new(ethereum::Ethash::new(params, From::from(ethash.params), builtins)),
ethjson::spec::Engine::BasicAuthority(basic_authority) => Box::new(BasicAuthority::new(params, From::from(basic_authority.params), builtins)),
}
}
@@ -241,15 +240,10 @@ impl Spec {
From::from(ethjson::spec::Spec::load(reader).expect("invalid json file"))
}
/// Create a new Spec which conforms to the Morden chain except that it's a NullEngine consensus.
/// Create a new Spec which conforms to the Frontier-era Morden chain except that it's a NullEngine consensus.
pub fn new_test() -> Spec {
Spec::load(include_bytes!("../../res/null_morden.json"))
}
/// Create a new Spec which conforms to the Morden chain except that it's a NullEngine consensus.
pub fn new_homestead_test() -> Spec {
Spec::load(include_bytes!("../../res/null_homestead_morden.json"))
}
}
#[cfg(test)]