Merge branch 'master' of github.com:ethcore/parity into tx-block
This commit is contained in:
@@ -21,8 +21,9 @@
|
||||
},
|
||||
"genesis": {
|
||||
"seal": {
|
||||
"generic": {
|
||||
"rlp": "0xc28080"
|
||||
"authority_round": {
|
||||
"step": "0x0",
|
||||
"signature": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
},
|
||||
"difficulty": "0x20000",
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
},
|
||||
"genesis": {
|
||||
"seal": {
|
||||
"generic": {
|
||||
"fields": 1,
|
||||
"rlp": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"
|
||||
}
|
||||
"generic": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"
|
||||
},
|
||||
"difficulty": "0x20000",
|
||||
"author": "0x0000000000000000000000000000000000000000",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Ethereum Classic",
|
||||
"forkName": "classic",
|
||||
"dataDir": "classic",
|
||||
"engine": {
|
||||
"Ethash": {
|
||||
"params": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Expanse",
|
||||
"forkName": "expanse",
|
||||
"dataDir": "expanse",
|
||||
"engine": {
|
||||
"Ethash": {
|
||||
"params": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "Frontier/Homestead",
|
||||
"dataDir": "ethereum",
|
||||
"engine": {
|
||||
"Ethash": {
|
||||
"params": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "Morden",
|
||||
"dataDir": "test",
|
||||
"engine": {
|
||||
"Ethash": {
|
||||
"params": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "Ropsten",
|
||||
"dataDir": "test",
|
||||
"engine": {
|
||||
"Ethash": {
|
||||
"params": {
|
||||
|
||||
@@ -4,29 +4,27 @@
|
||||
"InstantSeal": null
|
||||
},
|
||||
"params": {
|
||||
"accountStartNonce": "0x0100000",
|
||||
"accountStartNonce": "0x0",
|
||||
"maximumExtraDataSize": "0x20",
|
||||
"minGasLimit": "0x1388",
|
||||
"networkID" : "0x2"
|
||||
"networkID" : "0x11"
|
||||
},
|
||||
"genesis": {
|
||||
"seal": {
|
||||
"generic": {
|
||||
"rlp": "0x0"
|
||||
}
|
||||
"generic": "0x0"
|
||||
},
|
||||
"difficulty": "0x20000",
|
||||
"author": "0x0000000000000000000000000000000000000000",
|
||||
"timestamp": "0x00",
|
||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"extraData": "0x",
|
||||
"gasLimit": "0x2fefd8"
|
||||
"gasLimit": "0x5B8D80"
|
||||
},
|
||||
"accounts": {
|
||||
"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 } } } },
|
||||
"0x00a329c0648769a73afac7f9381e08fb43dbea72": { "balance": "1606938044258990275541962092341162602522202993782792835301376", "nonce": "1048576" }
|
||||
"0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
|
||||
"0000000000000000000000000000000000000002": { "balance": "1", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } },
|
||||
"0000000000000000000000000000000000000003": { "balance": "1", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } },
|
||||
"0000000000000000000000000000000000000004": { "balance": "1", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } },
|
||||
"0x00a329c0648769a73afac7f9381e08fb43dbea72": { "balance": "1606938044258990275541962092341162602522202993782792835301376" }
|
||||
}
|
||||
}
|
||||
|
||||
44
ethcore/res/tendermint.json
Normal file
44
ethcore/res/tendermint.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "TestBFT",
|
||||
"engine": {
|
||||
"Tendermint": {
|
||||
"params": {
|
||||
"gasLimitBoundDivisor": "0x0400",
|
||||
"authorities" : [
|
||||
"0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1",
|
||||
"0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"accountStartNonce": "0x0",
|
||||
"maximumExtraDataSize": "0x20",
|
||||
"minGasLimit": "0x1388",
|
||||
"networkID" : "0x2323"
|
||||
},
|
||||
"genesis": {
|
||||
"seal": {
|
||||
"tendermint": {
|
||||
"round": "0x0",
|
||||
"proposal": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
"precommits": [
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
]
|
||||
}
|
||||
},
|
||||
"difficulty": "0x20000",
|
||||
"author": "0x0000000000000000000000000000000000000000",
|
||||
"timestamp": "0x00",
|
||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"extraData": "0x",
|
||||
"gasLimit": "0x2fefd8"
|
||||
},
|
||||
"accounts": {
|
||||
"0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
|
||||
"0000000000000000000000000000000000000002": { "balance": "1", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } },
|
||||
"0000000000000000000000000000000000000003": { "balance": "1", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } },
|
||||
"0000000000000000000000000000000000000004": { "balance": "1", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } },
|
||||
"9cce34f7ab185c7aba1b7c8140d620b4bda941d6": { "balance": "1606938044258990275541962092341162602522202993782792835301376" }
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,14 @@
|
||||
|
||||
mod stores;
|
||||
|
||||
use self::stores::{AddressBook, DappsSettingsStore};
|
||||
use self::stores::{AddressBook, DappsSettingsStore, NewDappsPolicy};
|
||||
|
||||
use std::fmt;
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Instant, Duration};
|
||||
use util::{Mutex, RwLock};
|
||||
use ethstore::{SecretStore, Error as SSError, SafeAccount, EthStore};
|
||||
use ethstore::dir::{KeyDirectory};
|
||||
use util::RwLock;
|
||||
use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMultiStore, random_string};
|
||||
use ethstore::dir::MemoryDirectory;
|
||||
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
|
||||
use ethjson::misc::AccountMeta;
|
||||
pub use ethstore::ethkey::Signature;
|
||||
@@ -73,58 +73,47 @@ impl From<SSError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct NullDir {
|
||||
accounts: RwLock<HashMap<Address, SafeAccount>>,
|
||||
}
|
||||
|
||||
impl KeyDirectory for NullDir {
|
||||
fn load(&self) -> Result<Vec<SafeAccount>, SSError> {
|
||||
Ok(self.accounts.read().values().cloned().collect())
|
||||
}
|
||||
|
||||
fn insert(&self, account: SafeAccount) -> Result<SafeAccount, SSError> {
|
||||
self.accounts.write().insert(account.address.clone(), account.clone());
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
fn remove(&self, address: &Address) -> Result<(), SSError> {
|
||||
self.accounts.write().remove(address);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Dapp identifier
|
||||
pub type DappId = String;
|
||||
|
||||
fn transient_sstore() -> EthMultiStore {
|
||||
EthMultiStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")
|
||||
}
|
||||
|
||||
type AccountToken = String;
|
||||
|
||||
/// Account management.
|
||||
/// Responsible for unlocking accounts.
|
||||
pub struct AccountProvider {
|
||||
unlocked: Mutex<HashMap<Address, AccountData>>,
|
||||
sstore: Box<SecretStore>,
|
||||
unlocked: RwLock<HashMap<Address, AccountData>>,
|
||||
address_book: RwLock<AddressBook>,
|
||||
dapps_settings: RwLock<DappsSettingsStore>,
|
||||
/// Accounts on disk
|
||||
sstore: Box<SecretStore>,
|
||||
/// Accounts unlocked with rolling tokens
|
||||
transient_sstore: EthMultiStore,
|
||||
}
|
||||
|
||||
impl AccountProvider {
|
||||
/// Creates new account provider.
|
||||
pub fn new(sstore: Box<SecretStore>) -> Self {
|
||||
AccountProvider {
|
||||
unlocked: Mutex::new(HashMap::new()),
|
||||
unlocked: RwLock::new(HashMap::new()),
|
||||
address_book: RwLock::new(AddressBook::new(sstore.local_path().into())),
|
||||
dapps_settings: RwLock::new(DappsSettingsStore::new(sstore.local_path().into())),
|
||||
sstore: sstore,
|
||||
transient_sstore: transient_sstore(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates not disk backed provider.
|
||||
pub fn transient_provider() -> Self {
|
||||
AccountProvider {
|
||||
unlocked: Mutex::new(HashMap::new()),
|
||||
unlocked: RwLock::new(HashMap::new()),
|
||||
address_book: RwLock::new(AddressBook::transient()),
|
||||
dapps_settings: RwLock::new(DappsSettingsStore::transient()),
|
||||
sstore: Box::new(EthStore::open(Box::new(NullDir::default()))
|
||||
.expect("NullDir load always succeeds; qed"))
|
||||
sstore: Box::new(EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")),
|
||||
transient_sstore: transient_sstore(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,10 +156,49 @@ impl AccountProvider {
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
/// Sets a whitelist of accounts exposed for unknown dapps.
|
||||
/// `None` means that all accounts will be visible.
|
||||
pub fn set_new_dapps_whitelist(&self, accounts: Option<Vec<Address>>) -> Result<(), Error> {
|
||||
self.dapps_settings.write().set_policy(match accounts {
|
||||
None => NewDappsPolicy::AllAccounts,
|
||||
Some(accounts) => NewDappsPolicy::Whitelist(accounts),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets a whitelist of accounts exposed for unknown dapps.
|
||||
/// `None` means that all accounts will be visible.
|
||||
pub fn new_dapps_whitelist(&self) -> Result<Option<Vec<Address>>, Error> {
|
||||
Ok(match self.dapps_settings.read().policy() {
|
||||
NewDappsPolicy::AllAccounts => None,
|
||||
NewDappsPolicy::Whitelist(accounts) => Some(accounts),
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets a list of dapps recently requesting accounts.
|
||||
pub fn recent_dapps(&self) -> Result<Vec<DappId>, Error> {
|
||||
Ok(self.dapps_settings.read().recent_dapps())
|
||||
}
|
||||
|
||||
/// Marks dapp as recently used.
|
||||
pub fn note_dapp_used(&self, dapp: DappId) -> Result<(), Error> {
|
||||
let mut dapps = self.dapps_settings.write();
|
||||
dapps.mark_dapp_used(dapp.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets addresses visile for dapp.
|
||||
pub fn dapps_addresses(&self, dapp: DappId) -> Result<Vec<Address>, Error> {
|
||||
let accounts = self.dapps_settings.read().get();
|
||||
Ok(accounts.get(&dapp).map(|settings| settings.accounts.clone()).unwrap_or_else(Vec::new))
|
||||
let dapps = self.dapps_settings.read();
|
||||
|
||||
let accounts = dapps.settings().get(&dapp).map(|settings| settings.accounts.clone());
|
||||
match accounts {
|
||||
Some(accounts) => Ok(accounts),
|
||||
None => match dapps.policy() {
|
||||
NewDappsPolicy::AllAccounts => self.accounts(),
|
||||
NewDappsPolicy::Whitelist(accounts) => Ok(accounts),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets addresses visile for dapp.
|
||||
@@ -231,11 +259,8 @@ impl AccountProvider {
|
||||
|
||||
/// Returns `true` if the password for `account` is `password`. `false` if not.
|
||||
pub fn test_password(&self, account: &Address, password: &str) -> Result<bool, Error> {
|
||||
match self.sstore.sign(account, password, &Default::default()) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(SSError::InvalidPassword) => Ok(false),
|
||||
Err(e) => Err(Error::SStore(e)),
|
||||
}
|
||||
self.sstore.test_password(account, password)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Permanently removes an account.
|
||||
@@ -256,7 +281,7 @@ impl AccountProvider {
|
||||
let _ = try!(self.sstore.sign(&account, &password, &Default::default()));
|
||||
|
||||
// check if account is already unlocked pernamently, if it is, do nothing
|
||||
let mut unlocked = self.unlocked.lock();
|
||||
let mut unlocked = self.unlocked.write();
|
||||
if let Some(data) = unlocked.get(&account) {
|
||||
if let Unlock::Perm = data.unlock {
|
||||
return Ok(())
|
||||
@@ -273,7 +298,7 @@ impl AccountProvider {
|
||||
}
|
||||
|
||||
fn password(&self, account: &Address) -> Result<String, Error> {
|
||||
let mut unlocked = self.unlocked.lock();
|
||||
let mut unlocked = self.unlocked.write();
|
||||
let data = try!(unlocked.get(account).ok_or(Error::NotUnlocked)).clone();
|
||||
if let Unlock::Temp = data.unlock {
|
||||
unlocked.remove(account).expect("data exists: so key must exist: qed");
|
||||
@@ -304,7 +329,7 @@ impl AccountProvider {
|
||||
|
||||
/// Checks if given account is unlocked
|
||||
pub fn is_unlocked(&self, account: Address) -> bool {
|
||||
let unlocked = self.unlocked.lock();
|
||||
let unlocked = self.unlocked.read();
|
||||
unlocked.get(&account).is_some()
|
||||
}
|
||||
|
||||
@@ -314,6 +339,48 @@ impl AccountProvider {
|
||||
Ok(try!(self.sstore.sign(&account, &password, &message)))
|
||||
}
|
||||
|
||||
/// Signs given message with supplied token. Returns a token to use in next signing within this session.
|
||||
pub fn sign_with_token(&self, account: Address, token: AccountToken, message: Message) -> Result<(Signature, AccountToken), Error> {
|
||||
let is_std_password = try!(self.sstore.test_password(&account, &token));
|
||||
|
||||
let new_token = random_string(16);
|
||||
let signature = if is_std_password {
|
||||
// Insert to transient store
|
||||
try!(self.sstore.copy_account(&self.transient_sstore, &account, &token, &new_token));
|
||||
// sign
|
||||
try!(self.sstore.sign(&account, &token, &message))
|
||||
} else {
|
||||
// check transient store
|
||||
try!(self.transient_sstore.change_password(&account, &token, &new_token));
|
||||
// and sign
|
||||
try!(self.transient_sstore.sign(&account, &new_token, &message))
|
||||
};
|
||||
|
||||
Ok((signature, new_token))
|
||||
}
|
||||
|
||||
/// Decrypts a message with given token. Returns a token to use in next operation for this account.
|
||||
pub fn decrypt_with_token(&self, account: Address, token: AccountToken, shared_mac: &[u8], message: &[u8])
|
||||
-> Result<(Vec<u8>, AccountToken), Error>
|
||||
{
|
||||
let is_std_password = try!(self.sstore.test_password(&account, &token));
|
||||
|
||||
let new_token = random_string(16);
|
||||
let message = if is_std_password {
|
||||
// Insert to transient store
|
||||
try!(self.sstore.copy_account(&self.transient_sstore, &account, &token, &new_token));
|
||||
// decrypt
|
||||
try!(self.sstore.decrypt(&account, &token, shared_mac, message))
|
||||
} else {
|
||||
// check transient store
|
||||
try!(self.transient_sstore.change_password(&account, &token, &new_token));
|
||||
// and decrypt
|
||||
try!(self.transient_sstore.decrypt(&account, &token, shared_mac, message))
|
||||
};
|
||||
|
||||
Ok((message, new_token))
|
||||
}
|
||||
|
||||
/// Decrypts a message. If password is not provided the account must be unlocked.
|
||||
pub fn decrypt(&self, account: Address, password: Option<String>, shared_mac: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let password = try!(password.map(Ok).unwrap_or_else(|| self.password(&account)));
|
||||
@@ -370,15 +437,33 @@ mod tests {
|
||||
assert!(ap.unlock_account_timed(kp.address(), "test1".into(), 60000).is_err());
|
||||
assert!(ap.unlock_account_timed(kp.address(), "test".into(), 60000).is_ok());
|
||||
assert!(ap.sign(kp.address(), None, Default::default()).is_ok());
|
||||
ap.unlocked.lock().get_mut(&kp.address()).unwrap().unlock = Unlock::Timed(Instant::now());
|
||||
ap.unlocked.write().get_mut(&kp.address()).unwrap().unlock = Unlock::Timed(Instant::now());
|
||||
assert!(ap.sign(kp.address(), None, Default::default()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_sign_and_return_token() {
|
||||
// given
|
||||
let kp = Random.generate().unwrap();
|
||||
let ap = AccountProvider::transient_provider();
|
||||
assert!(ap.insert_account(kp.secret().clone(), "test").is_ok());
|
||||
|
||||
// when
|
||||
let (_signature, token) = ap.sign_with_token(kp.address(), "test".into(), Default::default()).unwrap();
|
||||
|
||||
// then
|
||||
ap.sign_with_token(kp.address(), token.clone(), Default::default())
|
||||
.expect("First usage of token should be correct.");
|
||||
assert!(ap.sign_with_token(kp.address(), token, Default::default()).is_err(), "Second usage of the same token should fail.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_set_dapps_addresses() {
|
||||
// given
|
||||
let ap = AccountProvider::transient_provider();
|
||||
let app = "app1".to_owned();
|
||||
// set `AllAccounts` policy
|
||||
ap.set_new_dapps_whitelist(None).unwrap();
|
||||
|
||||
// when
|
||||
ap.set_dapps_addresses(app.clone(), vec![1.into(), 2.into()]).unwrap();
|
||||
@@ -386,4 +471,23 @@ mod tests {
|
||||
// then
|
||||
assert_eq!(ap.dapps_addresses(app.clone()).unwrap(), vec![1.into(), 2.into()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_set_dapps_policy() {
|
||||
// given
|
||||
let ap = AccountProvider::transient_provider();
|
||||
let address = ap.new_account("test").unwrap();
|
||||
|
||||
// When returning nothing
|
||||
ap.set_new_dapps_whitelist(Some(vec![])).unwrap();
|
||||
assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![]);
|
||||
|
||||
// change to all
|
||||
ap.set_new_dapps_whitelist(None).unwrap();
|
||||
assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![address]);
|
||||
|
||||
// change to a whitelist
|
||||
ap.set_new_dapps_whitelist(Some(vec![1.into()])).unwrap();
|
||||
assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![1.into()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
//! Address Book and Dapps Settings Store
|
||||
|
||||
use std::{fs, fmt, hash, ops};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ethstore::ethkey::Address;
|
||||
use ethjson::misc::{AccountMeta, DappsSettings as JsonSettings};
|
||||
use ethjson::misc::{AccountMeta, DappsSettings as JsonSettings, NewDappsPolicy as JsonNewDappsPolicy};
|
||||
use account_provider::DappId;
|
||||
|
||||
/// Disk-backed map from Address to String. Uses JSON.
|
||||
@@ -105,43 +105,106 @@ impl From<DappsSettings> for JsonSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dapps user settings
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum NewDappsPolicy {
|
||||
AllAccounts,
|
||||
Whitelist(Vec<Address>),
|
||||
}
|
||||
|
||||
impl From<JsonNewDappsPolicy> for NewDappsPolicy {
|
||||
fn from(s: JsonNewDappsPolicy) -> Self {
|
||||
match s {
|
||||
JsonNewDappsPolicy::AllAccounts => NewDappsPolicy::AllAccounts,
|
||||
JsonNewDappsPolicy::Whitelist(accounts) => NewDappsPolicy::Whitelist(
|
||||
accounts.into_iter().map(Into::into).collect()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NewDappsPolicy> for JsonNewDappsPolicy {
|
||||
fn from(s: NewDappsPolicy) -> Self {
|
||||
match s {
|
||||
NewDappsPolicy::AllAccounts => JsonNewDappsPolicy::AllAccounts,
|
||||
NewDappsPolicy::Whitelist(accounts) => JsonNewDappsPolicy::Whitelist(
|
||||
accounts.into_iter().map(Into::into).collect()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_RECENT_DAPPS: usize = 10;
|
||||
|
||||
/// Disk-backed map from DappId to Settings. Uses JSON.
|
||||
pub struct DappsSettingsStore {
|
||||
cache: DiskMap<DappId, DappsSettings>,
|
||||
/// Dapps Settings
|
||||
settings: DiskMap<DappId, DappsSettings>,
|
||||
/// New Dapps Policy
|
||||
policy: DiskMap<String, NewDappsPolicy>,
|
||||
/// Recently Accessed Dapps (transient)
|
||||
recent: VecDeque<DappId>,
|
||||
}
|
||||
|
||||
impl DappsSettingsStore {
|
||||
/// Creates new store at given directory path.
|
||||
pub fn new(path: String) -> Self {
|
||||
let mut r = DappsSettingsStore {
|
||||
cache: DiskMap::new(path, "dapps_accounts.json".into())
|
||||
settings: DiskMap::new(path.clone(), "dapps_accounts.json".into()),
|
||||
policy: DiskMap::new(path.clone(), "dapps_policy.json".into()),
|
||||
recent: VecDeque::with_capacity(MAX_RECENT_DAPPS),
|
||||
};
|
||||
r.cache.revert(JsonSettings::read_dapps_settings);
|
||||
r.settings.revert(JsonSettings::read_dapps_settings);
|
||||
r.policy.revert(JsonNewDappsPolicy::read_new_dapps_policy);
|
||||
r
|
||||
}
|
||||
|
||||
/// Creates transient store (no changes are saved to disk).
|
||||
pub fn transient() -> Self {
|
||||
DappsSettingsStore {
|
||||
cache: DiskMap::transient()
|
||||
settings: DiskMap::transient(),
|
||||
policy: DiskMap::transient(),
|
||||
recent: VecDeque::with_capacity(MAX_RECENT_DAPPS),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get copy of the dapps settings
|
||||
pub fn get(&self) -> HashMap<DappId, DappsSettings> {
|
||||
self.cache.clone()
|
||||
pub fn settings(&self) -> HashMap<DappId, DappsSettings> {
|
||||
self.settings.clone()
|
||||
}
|
||||
|
||||
fn save(&self) {
|
||||
self.cache.save(JsonSettings::write_dapps_settings)
|
||||
/// Returns current new dapps policy
|
||||
pub fn policy(&self) -> NewDappsPolicy {
|
||||
self.policy.get("default").cloned().unwrap_or(NewDappsPolicy::AllAccounts)
|
||||
}
|
||||
|
||||
/// Returns recent dapps (in order of last request)
|
||||
pub fn recent_dapps(&self) -> Vec<DappId> {
|
||||
self.recent.iter().cloned().collect()
|
||||
}
|
||||
|
||||
/// Marks recent dapp as used
|
||||
pub fn mark_dapp_used(&mut self, dapp: DappId) {
|
||||
self.recent.retain(|id| id != &dapp);
|
||||
self.recent.push_front(dapp);
|
||||
while self.recent.len() > MAX_RECENT_DAPPS {
|
||||
self.recent.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets current new dapps policy
|
||||
pub fn set_policy(&mut self, policy: NewDappsPolicy) {
|
||||
self.policy.insert("default".into(), policy);
|
||||
self.policy.save(JsonNewDappsPolicy::write_new_dapps_policy);
|
||||
}
|
||||
|
||||
/// Sets accounts for specific dapp.
|
||||
pub fn set_accounts(&mut self, id: DappId, accounts: Vec<Address>) {
|
||||
{
|
||||
let mut settings = self.cache.entry(id).or_insert_with(DappsSettings::default);
|
||||
let mut settings = self.settings.entry(id).or_insert_with(DappsSettings::default);
|
||||
settings.accounts = accounts;
|
||||
}
|
||||
self.save();
|
||||
self.settings.save(JsonSettings::write_dapps_settings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +279,7 @@ impl<K: hash::Hash + Eq, V> DiskMap<K, V> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AddressBook, DappsSettingsStore, DappsSettings};
|
||||
use super::{AddressBook, DappsSettingsStore, DappsSettings, NewDappsPolicy};
|
||||
use std::collections::HashMap;
|
||||
use ethjson::misc::AccountMeta;
|
||||
use devtools::RandomTempPath;
|
||||
@@ -232,25 +295,6 @@ mod tests {
|
||||
assert_eq!(b.get(), hash_map![1.into() => AccountMeta{name: "One".to_owned(), meta: "{1:1}".to_owned(), uuid: None}]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_save_and_reload_dapps_settings() {
|
||||
// given
|
||||
let temp = RandomTempPath::create_dir();
|
||||
let path = temp.as_str().to_owned();
|
||||
let mut b = DappsSettingsStore::new(path.clone());
|
||||
|
||||
// when
|
||||
b.set_accounts("dappOne".into(), vec![1.into(), 2.into()]);
|
||||
|
||||
// then
|
||||
let b = DappsSettingsStore::new(path);
|
||||
assert_eq!(b.get(), hash_map![
|
||||
"dappOne".into() => DappsSettings {
|
||||
accounts: vec![1.into(), 2.into()],
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_remove_address() {
|
||||
let temp = RandomTempPath::create_dir();
|
||||
@@ -268,4 +312,58 @@ mod tests {
|
||||
3.into() => AccountMeta{name: "Three".to_owned(), meta: "{}".to_owned(), uuid: None}
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_save_and_reload_dapps_settings() {
|
||||
// given
|
||||
let temp = RandomTempPath::create_dir();
|
||||
let path = temp.as_str().to_owned();
|
||||
let mut b = DappsSettingsStore::new(path.clone());
|
||||
|
||||
// when
|
||||
b.set_accounts("dappOne".into(), vec![1.into(), 2.into()]);
|
||||
|
||||
// then
|
||||
let b = DappsSettingsStore::new(path);
|
||||
assert_eq!(b.settings(), hash_map![
|
||||
"dappOne".into() => DappsSettings {
|
||||
accounts: vec![1.into(), 2.into()],
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_maintain_a_list_of_recent_dapps() {
|
||||
let mut store = DappsSettingsStore::transient();
|
||||
assert!(store.recent_dapps().is_empty(), "Initially recent dapps should be empty.");
|
||||
|
||||
store.mark_dapp_used("dapp1".into());
|
||||
assert_eq!(store.recent_dapps(), vec!["dapp1".to_owned()]);
|
||||
|
||||
store.mark_dapp_used("dapp2".into());
|
||||
assert_eq!(store.recent_dapps(), vec!["dapp2".to_owned(), "dapp1".to_owned()]);
|
||||
|
||||
store.mark_dapp_used("dapp1".into());
|
||||
assert_eq!(store.recent_dapps(), vec!["dapp1".to_owned(), "dapp2".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_store_dapps_policy() {
|
||||
// given
|
||||
let temp = RandomTempPath::create_dir();
|
||||
let path = temp.as_str().to_owned();
|
||||
let mut store = DappsSettingsStore::new(path.clone());
|
||||
|
||||
// Test default policy
|
||||
assert_eq!(store.policy(), NewDappsPolicy::AllAccounts);
|
||||
|
||||
// when
|
||||
store.set_policy(NewDappsPolicy::Whitelist(vec![1.into(), 2.into()]));
|
||||
|
||||
// then
|
||||
let store = DappsSettingsStore::new(path);
|
||||
assert_eq!(store.policy.clone(), hash_map![
|
||||
"default".into() => NewDappsPolicy::Whitelist(vec![1.into(), 2.into()])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use ipc::IpcConfig;
|
||||
use util::H256;
|
||||
use util::{H256, Bytes};
|
||||
|
||||
/// Represents what has to be handled by actor listening to chain events
|
||||
#[ipc]
|
||||
@@ -27,6 +27,8 @@ pub trait ChainNotify : Send + Sync {
|
||||
_enacted: Vec<H256>,
|
||||
_retracted: Vec<H256>,
|
||||
_sealed: Vec<H256>,
|
||||
// Block bytes.
|
||||
_proposed: Vec<Bytes>,
|
||||
_duration: u64) {
|
||||
// does nothing by default
|
||||
}
|
||||
@@ -41,6 +43,9 @@ pub trait ChainNotify : Send + Sync {
|
||||
// does nothing by default
|
||||
}
|
||||
|
||||
/// fires when chain broadcasts a message
|
||||
fn broadcast(&self, _data: Vec<u8>) {}
|
||||
|
||||
/// fires when new transactions are received from a peer
|
||||
fn transactions_received(&self,
|
||||
_hashes: Vec<H256>,
|
||||
|
||||
@@ -24,8 +24,8 @@ use time::precise_time_ns;
|
||||
// util
|
||||
use util::{Bytes, PerfTimer, Itertools, Mutex, RwLock, Hashable};
|
||||
use util::{journaldb, TrieFactory, Trie};
|
||||
use util::trie::TrieSpec;
|
||||
use util::{U256, H256, Address, H2048, Uint, FixedHash};
|
||||
use util::trie::TrieSpec;
|
||||
use util::kvdb::*;
|
||||
|
||||
// other
|
||||
@@ -396,9 +396,10 @@ impl Client {
|
||||
/// This is triggered by a message coming from a block queue when the block is ready for insertion
|
||||
pub fn import_verified_blocks(&self) -> usize {
|
||||
let max_blocks_to_import = 4;
|
||||
let (imported_blocks, import_results, invalid_blocks, imported, duration, is_empty) = {
|
||||
let (imported_blocks, import_results, invalid_blocks, imported, proposed_blocks, duration, is_empty) = {
|
||||
let mut imported_blocks = Vec::with_capacity(max_blocks_to_import);
|
||||
let mut invalid_blocks = HashSet::new();
|
||||
let mut proposed_blocks = Vec::with_capacity(max_blocks_to_import);
|
||||
let mut import_results = Vec::with_capacity(max_blocks_to_import);
|
||||
|
||||
let _import_lock = self.import_lock.lock();
|
||||
@@ -417,12 +418,17 @@ impl Client {
|
||||
continue;
|
||||
}
|
||||
if let Ok(closed_block) = self.check_and_close_block(&block) {
|
||||
imported_blocks.push(header.hash());
|
||||
if self.engine.is_proposal(&block.header) {
|
||||
self.block_queue.mark_as_good(&[header.hash()]);
|
||||
proposed_blocks.push(block.bytes);
|
||||
} else {
|
||||
imported_blocks.push(header.hash());
|
||||
|
||||
let route = self.commit_block(closed_block, &header.hash(), &block.bytes);
|
||||
import_results.push(route);
|
||||
let route = self.commit_block(closed_block, &header.hash(), &block.bytes);
|
||||
import_results.push(route);
|
||||
|
||||
self.report.write().accrue_block(&block);
|
||||
self.report.write().accrue_block(&block);
|
||||
}
|
||||
} else {
|
||||
invalid_blocks.insert(header.hash());
|
||||
}
|
||||
@@ -436,7 +442,7 @@ impl Client {
|
||||
}
|
||||
let is_empty = self.block_queue.mark_as_good(&imported_blocks);
|
||||
let duration_ns = precise_time_ns() - start;
|
||||
(imported_blocks, import_results, invalid_blocks, imported, duration_ns, is_empty)
|
||||
(imported_blocks, import_results, invalid_blocks, imported, proposed_blocks, duration_ns, is_empty)
|
||||
};
|
||||
|
||||
{
|
||||
@@ -454,6 +460,7 @@ impl Client {
|
||||
enacted.clone(),
|
||||
retracted.clone(),
|
||||
Vec::new(),
|
||||
proposed_blocks.clone(),
|
||||
duration,
|
||||
);
|
||||
});
|
||||
@@ -577,9 +584,10 @@ impl Client {
|
||||
self.miner.clone()
|
||||
}
|
||||
|
||||
/// Used by PoA to try sealing on period change.
|
||||
pub fn update_sealing(&self) {
|
||||
self.miner.update_sealing(self)
|
||||
|
||||
/// Replace io channel. Useful for testing.
|
||||
pub fn set_io_channel(&self, io_channel: IoChannel<ClientIoMessage>) {
|
||||
*self.io_channel.lock() = io_channel;
|
||||
}
|
||||
|
||||
/// Attempt to get a copy of a specific block's final state.
|
||||
@@ -1290,6 +1298,18 @@ impl BlockChainClient for Client {
|
||||
self.miner.pending_transactions(self.chain.read().best_block_number())
|
||||
}
|
||||
|
||||
fn queue_consensus_message(&self, message: Bytes) {
|
||||
let channel = self.io_channel.lock().clone();
|
||||
if let Err(e) = channel.send(ClientIoMessage::NewMessage(message)) {
|
||||
debug!("Ignoring the message, error queueing: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn broadcast_consensus_message(&self, message: Bytes) {
|
||||
self.notify(|notify| notify.broadcast(message.clone()));
|
||||
}
|
||||
|
||||
|
||||
fn signing_network_id(&self) -> Option<u64> {
|
||||
self.engine.signing_network_id(&self.latest_env_info())
|
||||
}
|
||||
@@ -1314,7 +1334,6 @@ impl BlockChainClient for Client {
|
||||
}
|
||||
|
||||
impl MiningBlockChainClient for Client {
|
||||
|
||||
fn latest_schedule(&self) -> Schedule {
|
||||
self.engine.schedule(&self.latest_env_info())
|
||||
}
|
||||
@@ -1357,6 +1376,30 @@ impl MiningBlockChainClient for Client {
|
||||
&self.factories.vm
|
||||
}
|
||||
|
||||
fn update_sealing(&self) {
|
||||
self.miner.update_sealing(self)
|
||||
}
|
||||
|
||||
fn submit_seal(&self, block_hash: H256, seal: Vec<Bytes>) {
|
||||
if self.miner.submit_seal(self, block_hash, seal).is_err() {
|
||||
warn!(target: "poa", "Wrong internal seal submission!")
|
||||
}
|
||||
}
|
||||
|
||||
fn broadcast_proposal_block(&self, block: SealedBlock) {
|
||||
self.notify(|notify| {
|
||||
notify.new_blocks(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![block.rlp_bytes()],
|
||||
0,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn import_sealed_block(&self, block: SealedBlock) -> ImportResult {
|
||||
let h = block.header().hash();
|
||||
let start = precise_time_ns();
|
||||
@@ -1381,6 +1424,7 @@ impl MiningBlockChainClient for Client {
|
||||
enacted.clone(),
|
||||
retracted.clone(),
|
||||
vec![h.clone()],
|
||||
vec![],
|
||||
precise_time_ns() - start,
|
||||
);
|
||||
});
|
||||
@@ -1416,6 +1460,12 @@ impl ::client::ProvingBlockChainClient for Client {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Client {
|
||||
fn drop(&mut self) {
|
||||
self.engine.stop();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
|
||||
@@ -90,6 +90,8 @@ pub struct TestBlockChainClient {
|
||||
pub ancient_block: RwLock<Option<(H256, u64)>>,
|
||||
/// First block info.
|
||||
pub first_block: RwLock<Option<(H256, u64)>>,
|
||||
/// Traces to return
|
||||
pub traces: RwLock<Option<Vec<LocalizedTrace>>>,
|
||||
}
|
||||
|
||||
/// Used for generating test client blocks.
|
||||
@@ -151,6 +153,7 @@ impl TestBlockChainClient {
|
||||
latest_block_timestamp: RwLock::new(10_000_000),
|
||||
ancient_block: RwLock::new(None),
|
||||
first_block: RwLock::new(None),
|
||||
traces: RwLock::new(None),
|
||||
};
|
||||
client.add_blocks(1, EachBlockWith::Nothing); // add genesis block
|
||||
client.genesis_hash = client.last_hash.read().clone();
|
||||
@@ -360,6 +363,18 @@ impl MiningBlockChainClient for TestBlockChainClient {
|
||||
fn import_sealed_block(&self, _block: SealedBlock) -> ImportResult {
|
||||
Ok(H256::default())
|
||||
}
|
||||
|
||||
fn broadcast_proposal_block(&self, _block: SealedBlock) {}
|
||||
|
||||
fn update_sealing(&self) {
|
||||
self.miner.update_sealing(self)
|
||||
}
|
||||
|
||||
fn submit_seal(&self, block_hash: H256, seal: Vec<Bytes>) {
|
||||
if self.miner.submit_seal(self, block_hash, seal).is_err() {
|
||||
warn!(target: "poa", "Wrong internal seal submission!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockChainClient for TestBlockChainClient {
|
||||
@@ -642,19 +657,19 @@ impl BlockChainClient for TestBlockChainClient {
|
||||
}
|
||||
|
||||
fn filter_traces(&self, _filter: TraceFilter) -> Option<Vec<LocalizedTrace>> {
|
||||
unimplemented!();
|
||||
self.traces.read().clone()
|
||||
}
|
||||
|
||||
fn trace(&self, _trace: TraceId) -> Option<LocalizedTrace> {
|
||||
unimplemented!();
|
||||
self.traces.read().clone().and_then(|vec| vec.into_iter().next())
|
||||
}
|
||||
|
||||
fn transaction_traces(&self, _trace: TransactionId) -> Option<Vec<LocalizedTrace>> {
|
||||
unimplemented!();
|
||||
self.traces.read().clone()
|
||||
}
|
||||
|
||||
fn block_traces(&self, _trace: BlockId) -> Option<Vec<LocalizedTrace>> {
|
||||
unimplemented!();
|
||||
self.traces.read().clone()
|
||||
}
|
||||
|
||||
fn queue_transactions(&self, transactions: Vec<Bytes>, _peer_id: usize) {
|
||||
@@ -663,6 +678,12 @@ impl BlockChainClient for TestBlockChainClient {
|
||||
self.miner.import_external_transactions(self, txs);
|
||||
}
|
||||
|
||||
fn queue_consensus_message(&self, message: Bytes) {
|
||||
self.spec.engine.handle_message(&message).unwrap();
|
||||
}
|
||||
|
||||
fn broadcast_consensus_message(&self, _message: Bytes) {}
|
||||
|
||||
fn pending_transactions(&self) -> Vec<PendingTransaction> {
|
||||
self.miner.pending_transactions(self.chain_info().best_block_number)
|
||||
}
|
||||
|
||||
@@ -202,6 +202,12 @@ pub trait BlockChainClient : Sync + Send {
|
||||
/// Queue transactions for importing.
|
||||
fn queue_transactions(&self, transactions: Vec<Bytes>, peer_id: usize);
|
||||
|
||||
/// Queue conensus engine message.
|
||||
fn queue_consensus_message(&self, message: Bytes);
|
||||
|
||||
/// Used by PoA to communicate with peers.
|
||||
fn broadcast_consensus_message(&self, message: Bytes);
|
||||
|
||||
/// list all transactions
|
||||
fn pending_transactions(&self) -> Vec<PendingTransaction>;
|
||||
|
||||
@@ -273,6 +279,15 @@ pub trait MiningBlockChainClient: BlockChainClient {
|
||||
/// Returns EvmFactory.
|
||||
fn vm_factory(&self) -> &EvmFactory;
|
||||
|
||||
/// Used by PoA to try sealing on period change.
|
||||
fn update_sealing(&self);
|
||||
|
||||
/// Used by PoA to submit gathered signatures.
|
||||
fn submit_seal(&self, block_hash: H256, seal: Vec<Bytes>);
|
||||
|
||||
/// Broadcast a block proposal.
|
||||
fn broadcast_proposal_block(&self, block: SealedBlock);
|
||||
|
||||
/// Import sealed block. Skips all verifications.
|
||||
fn import_sealed_block(&self, block: SealedBlock) -> ImportResult;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ use rlp::{UntrustedRlp, Rlp, View, encode};
|
||||
use account_provider::AccountProvider;
|
||||
use block::*;
|
||||
use spec::CommonParams;
|
||||
use engines::Engine;
|
||||
use engines::{Engine, Seal, EngineError};
|
||||
use header::Header;
|
||||
use error::{Error, BlockError};
|
||||
use blockchain::extras::BlockDetails;
|
||||
@@ -225,8 +225,8 @@ impl Engine for AuthorityRound {
|
||||
///
|
||||
/// This operation is synchronous and may (quite reasonably) not be available, in which `false` will
|
||||
/// be returned.
|
||||
fn generate_seal(&self, block: &ExecutedBlock) -> Option<Vec<Bytes>> {
|
||||
if self.proposed.load(AtomicOrdering::SeqCst) { return None; }
|
||||
fn generate_seal(&self, block: &ExecutedBlock) -> Seal {
|
||||
if self.proposed.load(AtomicOrdering::SeqCst) { return Seal::None; }
|
||||
let header = block.header();
|
||||
let step = self.step();
|
||||
if self.is_step_proposer(step, header.author()) {
|
||||
@@ -235,7 +235,8 @@ impl Engine for AuthorityRound {
|
||||
if let Ok(signature) = ap.sign(*header.author(), self.password.read().clone(), header.bare_hash()) {
|
||||
trace!(target: "poa", "generate_seal: Issuing a block for step {}.", step);
|
||||
self.proposed.store(true, AtomicOrdering::SeqCst);
|
||||
return Some(vec![encode(&step).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
|
||||
let rlps = vec![encode(&step).to_vec(), encode(&(&*signature as &[u8])).to_vec()];
|
||||
return Seal::Regular(rlps);
|
||||
} else {
|
||||
warn!(target: "poa", "generate_seal: FAIL: Accounts secret key unavailable.");
|
||||
}
|
||||
@@ -245,7 +246,7 @@ impl Engine for AuthorityRound {
|
||||
} else {
|
||||
trace!(target: "poa", "generate_seal: Not a proposer for step {}.", step);
|
||||
}
|
||||
None
|
||||
Seal::None
|
||||
}
|
||||
|
||||
/// Check the number of seal fields.
|
||||
@@ -288,7 +289,7 @@ impl Engine for AuthorityRound {
|
||||
// Check if parent is from a previous step.
|
||||
if step == try!(header_step(parent)) {
|
||||
trace!(target: "poa", "Multiple blocks proposed for step {}.", step);
|
||||
try!(Err(BlockError::DoubleVote(header.author().clone())));
|
||||
try!(Err(EngineError::DoubleVote(header.author().clone())));
|
||||
}
|
||||
|
||||
let gas_limit_divisor = self.our_params.gas_limit_bound_divisor;
|
||||
@@ -347,6 +348,7 @@ mod tests {
|
||||
use tests::helpers::*;
|
||||
use account_provider::AccountProvider;
|
||||
use spec::Spec;
|
||||
use engines::Seal;
|
||||
|
||||
#[test]
|
||||
fn has_valid_metadata() {
|
||||
@@ -416,17 +418,17 @@ mod tests {
|
||||
let b2 = b2.close_and_lock();
|
||||
|
||||
engine.set_signer(addr1, "1".into());
|
||||
if let Some(seal) = engine.generate_seal(b1.block()) {
|
||||
if let Seal::Regular(seal) = engine.generate_seal(b1.block()) {
|
||||
assert!(b1.clone().try_seal(engine, seal).is_ok());
|
||||
// Second proposal is forbidden.
|
||||
assert!(engine.generate_seal(b1.block()).is_none());
|
||||
assert!(engine.generate_seal(b1.block()) == Seal::None);
|
||||
}
|
||||
|
||||
engine.set_signer(addr2, "2".into());
|
||||
if let Some(seal) = engine.generate_seal(b2.block()) {
|
||||
if let Seal::Regular(seal) = engine.generate_seal(b2.block()) {
|
||||
assert!(b2.clone().try_seal(engine, seal).is_ok());
|
||||
// Second proposal is forbidden.
|
||||
assert!(engine.generate_seal(b2.block()).is_none());
|
||||
assert!(engine.generate_seal(b2.block()) == Seal::None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ use account_provider::AccountProvider;
|
||||
use block::*;
|
||||
use builtin::Builtin;
|
||||
use spec::CommonParams;
|
||||
use engines::Engine;
|
||||
use engines::{Engine, Seal};
|
||||
use env_info::EnvInfo;
|
||||
use error::{BlockError, Error};
|
||||
use evm::Schedule;
|
||||
@@ -112,20 +112,20 @@ impl Engine for BasicAuthority {
|
||||
///
|
||||
/// This operation is synchronous and may (quite reasonably) not be available, in which `false` will
|
||||
/// be returned.
|
||||
fn generate_seal(&self, block: &ExecutedBlock) -> Option<Vec<Bytes>> {
|
||||
fn generate_seal(&self, block: &ExecutedBlock) -> Seal {
|
||||
if let Some(ref ap) = *self.account_provider.lock() {
|
||||
let header = block.header();
|
||||
let message = header.bare_hash();
|
||||
// account should be pernamently unlocked, otherwise sealing will fail
|
||||
if let Ok(signature) = ap.sign(*block.header().author(), self.password.read().clone(), message) {
|
||||
return Some(vec![::rlp::encode(&(&*signature as &[u8])).to_vec()]);
|
||||
return Seal::Regular(vec![::rlp::encode(&(&*signature as &[u8])).to_vec()]);
|
||||
} else {
|
||||
trace!(target: "basicauthority", "generate_seal: FAIL: accounts secret key unavailable");
|
||||
}
|
||||
} else {
|
||||
trace!(target: "basicauthority", "generate_seal: FAIL: accounts not provided");
|
||||
}
|
||||
None
|
||||
Seal::None
|
||||
}
|
||||
|
||||
fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> {
|
||||
@@ -199,6 +199,7 @@ mod tests {
|
||||
use account_provider::AccountProvider;
|
||||
use header::Header;
|
||||
use spec::Spec;
|
||||
use engines::Seal;
|
||||
|
||||
/// Create a new test chain spec with `BasicAuthority` consensus engine.
|
||||
fn new_test_authority() -> Spec {
|
||||
@@ -269,8 +270,9 @@ mod tests {
|
||||
let last_hashes = Arc::new(vec![genesis_header.hash()]);
|
||||
let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, addr, (3141562.into(), 31415620.into()), vec![]).unwrap();
|
||||
let b = b.close_and_lock();
|
||||
let seal = engine.generate_seal(b.block()).unwrap();
|
||||
assert!(b.try_seal(engine, seal).is_ok());
|
||||
if let Seal::Regular(seal) = engine.generate_seal(b.block()) {
|
||||
assert!(b.try_seal(engine, seal).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -17,12 +17,11 @@
|
||||
use std::collections::BTreeMap;
|
||||
use util::Address;
|
||||
use builtin::Builtin;
|
||||
use engines::Engine;
|
||||
use engines::{Engine, Seal};
|
||||
use env_info::EnvInfo;
|
||||
use spec::CommonParams;
|
||||
use evm::Schedule;
|
||||
use block::ExecutedBlock;
|
||||
use util::Bytes;
|
||||
|
||||
/// An engine which does not provide any consensus mechanism, just seals blocks internally.
|
||||
pub struct InstantSeal {
|
||||
@@ -54,13 +53,13 @@ impl Engine for InstantSeal {
|
||||
}
|
||||
|
||||
fn schedule(&self, _env_info: &EnvInfo) -> Schedule {
|
||||
Schedule::new_post_eip150(usize::max_value(), false, false, false)
|
||||
Schedule::new_post_eip150(usize::max_value(), true, true, true)
|
||||
}
|
||||
|
||||
fn is_sealer(&self, _author: &Address) -> Option<bool> { Some(true) }
|
||||
|
||||
fn generate_seal(&self, _block: &ExecutedBlock) -> Option<Vec<Bytes>> {
|
||||
Some(Vec::new())
|
||||
fn generate_seal(&self, _block: &ExecutedBlock) -> Seal {
|
||||
Seal::Regular(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +71,7 @@ mod tests {
|
||||
use spec::Spec;
|
||||
use header::Header;
|
||||
use block::*;
|
||||
use engines::Seal;
|
||||
|
||||
#[test]
|
||||
fn instant_can_seal() {
|
||||
@@ -84,8 +84,9 @@ mod tests {
|
||||
let last_hashes = Arc::new(vec![genesis_header.hash()]);
|
||||
let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, Address::default(), (3141562.into(), 31415620.into()), vec![]).unwrap();
|
||||
let b = b.close_and_lock();
|
||||
let seal = engine.generate_seal(b.block()).unwrap();
|
||||
assert!(b.try_seal(engine, seal).is_ok());
|
||||
if let Seal::Regular(seal) = engine.generate_seal(b.block()) {
|
||||
assert!(b.try_seal(engine, seal).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -20,11 +20,13 @@ mod null_engine;
|
||||
mod instant_seal;
|
||||
mod basic_authority;
|
||||
mod authority_round;
|
||||
mod tendermint;
|
||||
|
||||
pub use self::null_engine::NullEngine;
|
||||
pub use self::instant_seal::InstantSeal;
|
||||
pub use self::basic_authority::BasicAuthority;
|
||||
pub use self::authority_round::AuthorityRound;
|
||||
pub use self::tendermint::Tendermint;
|
||||
|
||||
use util::*;
|
||||
use account_provider::AccountProvider;
|
||||
@@ -42,6 +44,47 @@ use ethereum::ethash;
|
||||
use blockchain::extras::BlockDetails;
|
||||
use views::HeaderView;
|
||||
|
||||
/// Voting errors.
|
||||
#[derive(Debug)]
|
||||
pub enum EngineError {
|
||||
/// Signature does not belong to an authority.
|
||||
NotAuthorized(Address),
|
||||
/// The same author issued different votes at the same step.
|
||||
DoubleVote(Address),
|
||||
/// The received block is from an incorrect proposer.
|
||||
NotProposer(Mismatch<Address>),
|
||||
/// Message was not expected.
|
||||
UnexpectedMessage,
|
||||
/// Seal field has an unexpected size.
|
||||
BadSealFieldSize(OutOfBounds<usize>),
|
||||
}
|
||||
|
||||
impl fmt::Display for EngineError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use self::EngineError::*;
|
||||
let msg = match *self {
|
||||
DoubleVote(ref address) => format!("Author {} issued too many blocks.", address),
|
||||
NotProposer(ref mis) => format!("Author is not a current proposer: {}", mis),
|
||||
NotAuthorized(ref address) => format!("Signer {} is not authorized.", address),
|
||||
UnexpectedMessage => "This Engine should not be fed messages.".into(),
|
||||
BadSealFieldSize(ref oob) => format!("Seal field has an unexpected length: {}", oob),
|
||||
};
|
||||
|
||||
f.write_fmt(format_args!("Engine error ({})", msg))
|
||||
}
|
||||
}
|
||||
|
||||
/// Seal type.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Seal {
|
||||
/// Proposal seal; should be broadcasted, but not inserted into blockchain.
|
||||
Proposal(Vec<Bytes>),
|
||||
/// Regular block seal; should be part of the blockchain.
|
||||
Regular(Vec<Bytes>),
|
||||
/// Engine does generate seal for this block right now.
|
||||
None,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub trait Engine : Sync + Send {
|
||||
@@ -94,7 +137,7 @@ pub trait Engine : Sync + Send {
|
||||
///
|
||||
/// This operation is synchronous and may (quite reasonably) not be available, in which None will
|
||||
/// be returned.
|
||||
fn generate_seal(&self, _block: &ExecutedBlock) -> Option<Vec<Bytes>> { None }
|
||||
fn generate_seal(&self, _block: &ExecutedBlock) -> Seal { Seal::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.
|
||||
@@ -133,6 +176,10 @@ pub trait Engine : Sync + Send {
|
||||
header.set_gas_limit(parent.gas_limit().clone());
|
||||
}
|
||||
|
||||
/// Handle any potential consensus messages;
|
||||
/// updating consensus state and potentially issuing a new one.
|
||||
fn handle_message(&self, _message: &[u8]) -> Result<(), Error> { Err(EngineError::UnexpectedMessage.into()) }
|
||||
|
||||
// TODO: builtin contract routing - to do this properly, it will require removing the built-in configuration-reading logic
|
||||
// from Spec into here and removing the Spec::builtins field.
|
||||
/// Determine whether a particular address is a builtin contract.
|
||||
@@ -153,9 +200,16 @@ pub trait Engine : Sync + Send {
|
||||
ethash::is_new_best_block(best_total_difficulty, parent_details, new_header)
|
||||
}
|
||||
|
||||
/// Find out if the block is a proposal block and should not be inserted into the DB.
|
||||
/// Takes a header of a fully verified block.
|
||||
fn is_proposal(&self, _verified_header: &Header) -> bool { false }
|
||||
|
||||
/// Register an account which signs consensus messages.
|
||||
fn set_signer(&self, _address: Address, _password: String) {}
|
||||
|
||||
/// Stops any services that the may hold the Engine and makes it safe to drop.
|
||||
fn stop(&self) {}
|
||||
|
||||
/// Add a channel for communication with Client which can be used for sealing.
|
||||
fn register_message_channel(&self, _message_channel: IoChannel<ClientIoMessage>) {}
|
||||
|
||||
|
||||
279
ethcore/src/engines/tendermint/message.rs
Normal file
279
ethcore/src/engines/tendermint/message.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
// 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/>.
|
||||
|
||||
//! Tendermint message handling.
|
||||
|
||||
use util::*;
|
||||
use super::{Height, Round, BlockHash, Step};
|
||||
use error::Error;
|
||||
use header::Header;
|
||||
use rlp::*;
|
||||
use ethkey::{recover, public_to_address};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct ConsensusMessage {
|
||||
pub signature: H520,
|
||||
pub height: Height,
|
||||
pub round: Round,
|
||||
pub step: Step,
|
||||
pub block_hash: Option<BlockHash>,
|
||||
}
|
||||
|
||||
|
||||
fn consensus_round(header: &Header) -> Result<Round, ::rlp::DecoderError> {
|
||||
let round_rlp = header.seal().get(0).expect("seal passed basic verification; seal has 3 fields; qed");
|
||||
UntrustedRlp::new(round_rlp.as_slice()).as_val()
|
||||
}
|
||||
|
||||
impl ConsensusMessage {
|
||||
pub fn new(signature: H520, height: Height, round: Round, step: Step, block_hash: Option<BlockHash>) -> Self {
|
||||
ConsensusMessage {
|
||||
signature: signature,
|
||||
height: height,
|
||||
round: round,
|
||||
step: step,
|
||||
block_hash: block_hash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_proposal(header: &Header) -> Result<Self, ::rlp::DecoderError> {
|
||||
Ok(ConsensusMessage {
|
||||
signature: try!(UntrustedRlp::new(header.seal().get(1).expect("seal passed basic verification; seal has 3 fields; qed").as_slice()).as_val()),
|
||||
height: header.number() as Height,
|
||||
round: try!(consensus_round(header)),
|
||||
step: Step::Propose,
|
||||
block_hash: Some(header.bare_hash()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_commit(proposal: &ConsensusMessage, signature: H520) -> Self {
|
||||
ConsensusMessage {
|
||||
signature: signature,
|
||||
height: proposal.height,
|
||||
round: proposal.round,
|
||||
step: Step::Precommit,
|
||||
block_hash: proposal.block_hash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_height(&self, height: Height) -> bool {
|
||||
self.height == height
|
||||
}
|
||||
|
||||
pub fn is_round(&self, height: Height, round: Round) -> bool {
|
||||
self.height == height && self.round == round
|
||||
}
|
||||
|
||||
pub fn is_step(&self, height: Height, round: Round, step: Step) -> bool {
|
||||
self.height == height && self.round == round && self.step == step
|
||||
}
|
||||
|
||||
pub fn is_block_hash(&self, h: Height, r: Round, s: Step, block_hash: Option<BlockHash>) -> bool {
|
||||
self.height == h && self.round == r && self.step == s && self.block_hash == block_hash
|
||||
}
|
||||
|
||||
pub fn is_aligned(&self, m: &ConsensusMessage) -> bool {
|
||||
self.is_block_hash(m.height, m.round, m.step, m.block_hash)
|
||||
}
|
||||
|
||||
pub fn verify(&self) -> Result<Address, Error> {
|
||||
let full_rlp = ::rlp::encode(self);
|
||||
let block_info = Rlp::new(&full_rlp).at(1);
|
||||
let public_key = try!(recover(&self.signature.into(), &block_info.as_raw().sha3()));
|
||||
Ok(public_to_address(&public_key))
|
||||
}
|
||||
|
||||
pub fn precommit_hash(&self) -> H256 {
|
||||
message_info_rlp(self.height, self.round, Step::Precommit, self.block_hash).sha3()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ConsensusMessage {
|
||||
fn partial_cmp(&self, m: &ConsensusMessage) -> Option<Ordering> {
|
||||
Some(self.cmp(m))
|
||||
}
|
||||
}
|
||||
|
||||
impl Step {
|
||||
fn number(&self) -> u8 {
|
||||
match *self {
|
||||
Step::Propose => 0,
|
||||
Step::Prevote => 1,
|
||||
Step::Precommit => 2,
|
||||
Step::Commit => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for ConsensusMessage {
|
||||
fn cmp(&self, m: &ConsensusMessage) -> Ordering {
|
||||
if self.height != m.height {
|
||||
self.height.cmp(&m.height)
|
||||
} else if self.round != m.round {
|
||||
self.round.cmp(&m.round)
|
||||
} else if self.step != m.step {
|
||||
self.step.number().cmp(&m.step.number())
|
||||
} else {
|
||||
self.signature.cmp(&m.signature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for Step {
|
||||
fn decode<D>(decoder: &D) -> Result<Self, DecoderError> where D: Decoder {
|
||||
match try!(decoder.as_rlp().as_val()) {
|
||||
0u8 => Ok(Step::Propose),
|
||||
1 => Ok(Step::Prevote),
|
||||
2 => Ok(Step::Precommit),
|
||||
_ => Err(DecoderError::Custom("Invalid step.")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for Step {
|
||||
fn rlp_append(&self, s: &mut RlpStream) {
|
||||
s.append(&self.number());
|
||||
}
|
||||
}
|
||||
|
||||
/// (signature, height, round, step, block_hash)
|
||||
impl Decodable for ConsensusMessage {
|
||||
fn decode<D>(decoder: &D) -> Result<Self, DecoderError> where D: Decoder {
|
||||
let rlp = decoder.as_rlp();
|
||||
let m = try!(rlp.at(1));
|
||||
let block_message: H256 = try!(m.val_at(3));
|
||||
Ok(ConsensusMessage {
|
||||
signature: try!(rlp.val_at(0)),
|
||||
height: try!(m.val_at(0)),
|
||||
round: try!(m.val_at(1)),
|
||||
step: try!(m.val_at(2)),
|
||||
block_hash: match block_message.is_zero() {
|
||||
true => None,
|
||||
false => Some(block_message),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for ConsensusMessage {
|
||||
fn rlp_append(&self, s: &mut RlpStream) {
|
||||
let info = message_info_rlp(self.height, self.round, self.step, self.block_hash);
|
||||
s.begin_list(2)
|
||||
.append(&self.signature)
|
||||
.append_raw(&info, 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message_info_rlp(height: Height, round: Round, step: Step, block_hash: Option<BlockHash>) -> Bytes {
|
||||
// TODO: figure out whats wrong with nested list encoding
|
||||
let mut s = RlpStream::new_list(5);
|
||||
s.append(&height).append(&round).append(&step).append(&block_hash.unwrap_or_else(H256::zero));
|
||||
s.out()
|
||||
}
|
||||
|
||||
|
||||
pub fn message_full_rlp(signature: &H520, vote_info: &Bytes) -> Bytes {
|
||||
let mut s = RlpStream::new_list(2);
|
||||
s.append(signature).append_raw(vote_info, 1);
|
||||
s.out()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use util::*;
|
||||
use rlp::*;
|
||||
use super::super::Step;
|
||||
use super::*;
|
||||
use account_provider::AccountProvider;
|
||||
use header::Header;
|
||||
|
||||
#[test]
|
||||
fn encode_decode() {
|
||||
let message = ConsensusMessage {
|
||||
signature: H520::default(),
|
||||
height: 10,
|
||||
round: 123,
|
||||
step: Step::Precommit,
|
||||
block_hash: Some("1".sha3())
|
||||
};
|
||||
let raw_rlp = ::rlp::encode(&message).to_vec();
|
||||
let rlp = Rlp::new(&raw_rlp);
|
||||
assert_eq!(message, rlp.as_val());
|
||||
|
||||
let message = ConsensusMessage {
|
||||
signature: H520::default(),
|
||||
height: 1314,
|
||||
round: 0,
|
||||
step: Step::Prevote,
|
||||
block_hash: None
|
||||
};
|
||||
let raw_rlp = ::rlp::encode(&message);
|
||||
let rlp = Rlp::new(&raw_rlp);
|
||||
assert_eq!(message, rlp.as_val());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_and_verify() {
|
||||
let tap = Arc::new(AccountProvider::transient_provider());
|
||||
let addr = tap.insert_account("0".sha3(), "0").unwrap();
|
||||
tap.unlock_account_permanently(addr, "0".into()).unwrap();
|
||||
|
||||
let mi = message_info_rlp(123, 2, Step::Precommit, Some(H256::default()));
|
||||
|
||||
let raw_rlp = message_full_rlp(&tap.sign(addr, None, mi.sha3()).unwrap().into(), &mi);
|
||||
|
||||
let rlp = UntrustedRlp::new(&raw_rlp);
|
||||
let message: ConsensusMessage = rlp.as_val().unwrap();
|
||||
match message.verify() { Ok(a) if a == addr => {}, _ => panic!(), };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proposal_message() {
|
||||
let mut header = Header::default();
|
||||
let seal = vec![
|
||||
::rlp::encode(&0u8).to_vec(),
|
||||
::rlp::encode(&H520::default()).to_vec(),
|
||||
Vec::new()
|
||||
];
|
||||
header.set_seal(seal);
|
||||
let message = ConsensusMessage::new_proposal(&header).unwrap();
|
||||
assert_eq!(
|
||||
message,
|
||||
ConsensusMessage {
|
||||
signature: Default::default(),
|
||||
height: 0,
|
||||
round: 0,
|
||||
step: Step::Propose,
|
||||
block_hash: Some(header.bare_hash())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_info_from_header() {
|
||||
let header = Header::default();
|
||||
let pro = ConsensusMessage {
|
||||
signature: Default::default(),
|
||||
height: 0,
|
||||
round: 0,
|
||||
step: Step::Propose,
|
||||
block_hash: Some(header.bare_hash())
|
||||
};
|
||||
let pre = message_info_rlp(0, 0, Step::Precommit, Some(header.bare_hash()));
|
||||
|
||||
assert_eq!(pro.precommit_hash(), pre.sha3());
|
||||
}
|
||||
}
|
||||
962
ethcore/src/engines/tendermint/mod.rs
Normal file
962
ethcore/src/engines/tendermint/mod.rs
Normal file
@@ -0,0 +1,962 @@
|
||||
// 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/>.
|
||||
|
||||
/// Tendermint BFT consensus engine with round robin proof-of-authority.
|
||||
/// At each blockchain `Height` there can be multiple `Round`s of voting.
|
||||
/// Signatures always sign `Height`, `Round`, `Step` and `BlockHash` which is a block hash without seal.
|
||||
/// First a block with `Seal::Proposal` is issued by the designated proposer.
|
||||
/// Next the `Round` proceeds through `Prevote` and `Precommit` `Step`s.
|
||||
/// Block is issued when there is enough `Precommit` votes collected on a particular block at the end of a `Round`.
|
||||
/// Once enough votes have been gathered the proposer issues that block in the `Commit` step.
|
||||
|
||||
mod message;
|
||||
mod transition;
|
||||
mod params;
|
||||
mod vote_collector;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
|
||||
use util::*;
|
||||
use error::{Error, BlockError};
|
||||
use header::Header;
|
||||
use builtin::Builtin;
|
||||
use env_info::EnvInfo;
|
||||
use transaction::SignedTransaction;
|
||||
use rlp::{UntrustedRlp, View};
|
||||
use ethkey::{recover, public_to_address};
|
||||
use account_provider::AccountProvider;
|
||||
use block::*;
|
||||
use spec::CommonParams;
|
||||
use engines::{Engine, Seal, EngineError};
|
||||
use blockchain::extras::BlockDetails;
|
||||
use views::HeaderView;
|
||||
use evm::Schedule;
|
||||
use io::{IoService, IoChannel};
|
||||
use service::ClientIoMessage;
|
||||
use self::message::*;
|
||||
use self::transition::TransitionHandler;
|
||||
use self::params::TendermintParams;
|
||||
use self::vote_collector::VoteCollector;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Step {
|
||||
Propose,
|
||||
Prevote,
|
||||
Precommit,
|
||||
Commit
|
||||
}
|
||||
|
||||
impl Step {
|
||||
pub fn is_pre(self) -> bool {
|
||||
match self {
|
||||
Step::Prevote | Step::Precommit => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Height = usize;
|
||||
pub type Round = usize;
|
||||
pub type BlockHash = H256;
|
||||
|
||||
/// Engine using `Tendermint` consensus algorithm, suitable for EVM chain.
|
||||
pub struct Tendermint {
|
||||
params: CommonParams,
|
||||
our_params: TendermintParams,
|
||||
builtins: BTreeMap<Address, Builtin>,
|
||||
step_service: IoService<Step>,
|
||||
/// Address to be used as authority.
|
||||
authority: RwLock<Address>,
|
||||
/// Password used for signing messages.
|
||||
password: RwLock<Option<String>>,
|
||||
/// Blockchain height.
|
||||
height: AtomicUsize,
|
||||
/// Consensus round.
|
||||
round: AtomicUsize,
|
||||
/// Consensus step.
|
||||
step: RwLock<Step>,
|
||||
/// Vote accumulator.
|
||||
votes: VoteCollector,
|
||||
/// Channel for updating the sealing.
|
||||
message_channel: Mutex<Option<IoChannel<ClientIoMessage>>>,
|
||||
/// Used to sign messages and proposals.
|
||||
account_provider: Mutex<Option<Arc<AccountProvider>>>,
|
||||
/// Message for the last PoLC.
|
||||
lock_change: RwLock<Option<ConsensusMessage>>,
|
||||
/// Last lock round.
|
||||
last_lock: AtomicUsize,
|
||||
/// Bare hash of the proposed block, used for seal submission.
|
||||
proposal: RwLock<Option<H256>>,
|
||||
}
|
||||
|
||||
impl Tendermint {
|
||||
/// Create a new instance of Tendermint engine
|
||||
pub fn new(params: CommonParams, our_params: TendermintParams, builtins: BTreeMap<Address, Builtin>) -> Result<Arc<Self>, Error> {
|
||||
let engine = Arc::new(
|
||||
Tendermint {
|
||||
params: params,
|
||||
our_params: our_params,
|
||||
builtins: builtins,
|
||||
step_service: try!(IoService::<Step>::start()),
|
||||
authority: RwLock::new(Address::default()),
|
||||
password: RwLock::new(None),
|
||||
height: AtomicUsize::new(1),
|
||||
round: AtomicUsize::new(0),
|
||||
step: RwLock::new(Step::Propose),
|
||||
votes: VoteCollector::new(),
|
||||
message_channel: Mutex::new(None),
|
||||
account_provider: Mutex::new(None),
|
||||
lock_change: RwLock::new(None),
|
||||
last_lock: AtomicUsize::new(0),
|
||||
proposal: RwLock::new(None),
|
||||
});
|
||||
let handler = TransitionHandler { engine: Arc::downgrade(&engine) };
|
||||
try!(engine.step_service.register_handler(Arc::new(handler)));
|
||||
Ok(engine)
|
||||
}
|
||||
|
||||
fn update_sealing(&self) {
|
||||
if let Some(ref channel) = *self.message_channel.lock() {
|
||||
match channel.send(ClientIoMessage::UpdateSealing) {
|
||||
Ok(_) => trace!(target: "poa", "UpdateSealing message sent."),
|
||||
Err(err) => warn!(target: "poa", "Could not send a sealing message {}.", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_seal(&self, block_hash: H256, seal: Vec<Bytes>) {
|
||||
if let Some(ref channel) = *self.message_channel.lock() {
|
||||
match channel.send(ClientIoMessage::SubmitSeal(block_hash, seal)) {
|
||||
Ok(_) => trace!(target: "poa", "SubmitSeal message sent."),
|
||||
Err(err) => warn!(target: "poa", "Could not send a sealing message {}.", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn broadcast_message(&self, message: Bytes) {
|
||||
let channel = self.message_channel.lock().clone();
|
||||
if let Some(ref channel) = channel {
|
||||
match channel.send(ClientIoMessage::BroadcastMessage(message)) {
|
||||
Ok(_) => trace!(target: "poa", "BroadcastMessage message sent."),
|
||||
Err(err) => warn!(target: "poa", "broadcast_message: Could not send a sealing message {}.", err),
|
||||
}
|
||||
} else {
|
||||
warn!(target: "poa", "broadcast_message: No IoChannel available.");
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_message(&self, block_hash: Option<BlockHash>) -> Option<Bytes> {
|
||||
if let Some(ref ap) = *self.account_provider.lock() {
|
||||
let h = self.height.load(AtomicOrdering::SeqCst);
|
||||
let r = self.round.load(AtomicOrdering::SeqCst);
|
||||
let s = self.step.read();
|
||||
let vote_info = message_info_rlp(h, r, *s, block_hash);
|
||||
let authority = self.authority.read();
|
||||
match ap.sign(*authority, self.password.read().clone(), vote_info.sha3()).map(Into::into) {
|
||||
Ok(signature) => {
|
||||
let message_rlp = message_full_rlp(&signature, &vote_info);
|
||||
let message = ConsensusMessage::new(signature, h, r, *s, block_hash);
|
||||
self.votes.vote(message.clone(), *authority);
|
||||
debug!(target: "poa", "Generated {:?} as {}.", message, *authority);
|
||||
self.handle_valid_message(&message);
|
||||
|
||||
Some(message_rlp)
|
||||
},
|
||||
Err(e) => {
|
||||
trace!(target: "poa", "Could not sign the message {}", e);
|
||||
None
|
||||
},
|
||||
}
|
||||
} else {
|
||||
warn!(target: "poa", "No AccountProvider available.");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_and_broadcast_message(&self, block_hash: Option<BlockHash>) {
|
||||
if let Some(message) = self.generate_message(block_hash) {
|
||||
self.broadcast_message(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Broadcast all messages since last issued block to get the peers up to speed.
|
||||
fn broadcast_old_messages(&self) {
|
||||
for m in self.votes.get_up_to(self.height.load(AtomicOrdering::SeqCst)).into_iter() {
|
||||
self.broadcast_message(m);
|
||||
}
|
||||
}
|
||||
|
||||
fn to_next_height(&self, height: Height) {
|
||||
let new_height = height + 1;
|
||||
debug!(target: "poa", "Received a Commit, transitioning to height {}.", new_height);
|
||||
self.last_lock.store(0, AtomicOrdering::SeqCst);
|
||||
self.height.store(new_height, AtomicOrdering::SeqCst);
|
||||
self.round.store(0, AtomicOrdering::SeqCst);
|
||||
*self.lock_change.write() = None;
|
||||
}
|
||||
|
||||
/// Use via step_service to transition steps.
|
||||
fn to_step(&self, step: Step) {
|
||||
if let Err(io_err) = self.step_service.send_message(step) {
|
||||
warn!(target: "poa", "Could not proceed to step {}.", io_err)
|
||||
}
|
||||
*self.step.write() = step;
|
||||
match step {
|
||||
Step::Propose => {
|
||||
*self.proposal.write() = None;
|
||||
self.update_sealing()
|
||||
},
|
||||
Step::Prevote => {
|
||||
let block_hash = match *self.lock_change.read() {
|
||||
Some(ref m) if !self.should_unlock(m.round) => m.block_hash,
|
||||
_ => self.proposal.read().clone(),
|
||||
};
|
||||
self.generate_and_broadcast_message(block_hash);
|
||||
},
|
||||
Step::Precommit => {
|
||||
trace!(target: "poa", "to_step: Precommit.");
|
||||
let block_hash = match *self.lock_change.read() {
|
||||
Some(ref m) if self.is_round(m) && m.block_hash.is_some() => {
|
||||
trace!(target: "poa", "Setting last lock: {}", m.round);
|
||||
self.last_lock.store(m.round, AtomicOrdering::SeqCst);
|
||||
m.block_hash
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
self.generate_and_broadcast_message(block_hash);
|
||||
},
|
||||
Step::Commit => {
|
||||
trace!(target: "poa", "to_step: Commit.");
|
||||
// Commit the block using a complete signature set.
|
||||
let round = self.round.load(AtomicOrdering::SeqCst);
|
||||
let height = self.height.load(AtomicOrdering::SeqCst);
|
||||
if let Some(block_hash) = *self.proposal.read() {
|
||||
// Generate seal and remove old votes.
|
||||
if self.is_proposer(&*self.authority.read()).is_ok() {
|
||||
if let Some(seal) = self.votes.seal_signatures(height, round, block_hash) {
|
||||
trace!(target: "poa", "Collected seal: {:?}", seal);
|
||||
let seal = vec![
|
||||
::rlp::encode(&round).to_vec(),
|
||||
::rlp::encode(&seal.proposal).to_vec(),
|
||||
::rlp::encode(&seal.votes).to_vec()
|
||||
];
|
||||
self.submit_seal(block_hash, seal);
|
||||
self.to_next_height(height);
|
||||
} else {
|
||||
warn!(target: "poa", "Not enough votes found!");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn is_authority(&self, address: &Address) -> bool {
|
||||
self.our_params.authorities.contains(address)
|
||||
}
|
||||
|
||||
fn is_above_threshold(&self, n: usize) -> bool {
|
||||
n > self.our_params.authority_n * 2/3
|
||||
}
|
||||
|
||||
/// Check if address is a proposer for given round.
|
||||
fn is_round_proposer(&self, height: Height, round: Round, address: &Address) -> Result<(), EngineError> {
|
||||
let ref p = self.our_params;
|
||||
let proposer_nonce = height + round;
|
||||
trace!(target: "poa", "is_proposer: Proposer nonce: {}", proposer_nonce);
|
||||
let proposer = p.authorities.get(proposer_nonce % p.authority_n).expect("There are authority_n authorities; taking number modulo authority_n gives number in authority_n range; qed");
|
||||
if proposer == address {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(EngineError::NotProposer(Mismatch { expected: proposer.clone(), found: address.clone() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if address is the current proposer.
|
||||
fn is_proposer(&self, address: &Address) -> Result<(), EngineError> {
|
||||
self.is_round_proposer(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst), address)
|
||||
}
|
||||
|
||||
fn is_height(&self, message: &ConsensusMessage) -> bool {
|
||||
message.is_height(self.height.load(AtomicOrdering::SeqCst))
|
||||
}
|
||||
|
||||
fn is_round(&self, message: &ConsensusMessage) -> bool {
|
||||
message.is_round(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst))
|
||||
}
|
||||
|
||||
fn increment_round(&self, n: Round) {
|
||||
trace!(target: "poa", "increment_round: New round.");
|
||||
self.round.fetch_add(n, AtomicOrdering::SeqCst);
|
||||
}
|
||||
|
||||
fn should_unlock(&self, lock_change_round: Round) -> bool {
|
||||
self.last_lock.load(AtomicOrdering::SeqCst) < lock_change_round
|
||||
&& lock_change_round < self.round.load(AtomicOrdering::SeqCst)
|
||||
}
|
||||
|
||||
|
||||
fn has_enough_any_votes(&self) -> bool {
|
||||
let step_votes = self.votes.count_step_votes(self.height.load(AtomicOrdering::SeqCst), self.round.load(AtomicOrdering::SeqCst), *self.step.read());
|
||||
self.is_above_threshold(step_votes)
|
||||
}
|
||||
|
||||
fn has_enough_future_step_votes(&self, message: &ConsensusMessage) -> bool {
|
||||
if message.round > self.round.load(AtomicOrdering::SeqCst) {
|
||||
let step_votes = self.votes.count_step_votes(message.height, message.round, message.step);
|
||||
self.is_above_threshold(step_votes)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn has_enough_aligned_votes(&self, message: &ConsensusMessage) -> bool {
|
||||
let aligned_count = self.votes.count_aligned_votes(&message);
|
||||
self.is_above_threshold(aligned_count)
|
||||
}
|
||||
|
||||
fn handle_valid_message(&self, message: &ConsensusMessage) {
|
||||
let is_newer_than_lock = match *self.lock_change.read() {
|
||||
Some(ref lock) => message > lock,
|
||||
None => true,
|
||||
};
|
||||
let lock_change = is_newer_than_lock
|
||||
&& message.step == Step::Prevote
|
||||
&& message.block_hash.is_some()
|
||||
&& self.has_enough_aligned_votes(message);
|
||||
if lock_change {
|
||||
trace!(target: "poa", "handle_valid_message: Lock change.");
|
||||
*self.lock_change.write() = Some(message.clone());
|
||||
}
|
||||
// Check if it can affect the step transition.
|
||||
if self.is_height(message) {
|
||||
let next_step = match *self.step.read() {
|
||||
Step::Precommit if self.has_enough_aligned_votes(message) => {
|
||||
if message.block_hash.is_none() {
|
||||
self.increment_round(1);
|
||||
Some(Step::Propose)
|
||||
} else {
|
||||
Some(Step::Commit)
|
||||
}
|
||||
},
|
||||
Step::Precommit if self.has_enough_future_step_votes(message) => {
|
||||
self.increment_round(message.round - self.round.load(AtomicOrdering::SeqCst));
|
||||
Some(Step::Precommit)
|
||||
},
|
||||
// Avoid counting twice.
|
||||
Step::Prevote if lock_change => Some(Step::Precommit),
|
||||
Step::Prevote if self.has_enough_aligned_votes(message) => Some(Step::Precommit),
|
||||
Step::Prevote if self.has_enough_future_step_votes(message) => {
|
||||
self.increment_round(message.round - self.round.load(AtomicOrdering::SeqCst));
|
||||
Some(Step::Prevote)
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(step) = next_step {
|
||||
trace!(target: "poa", "Transition to {:?} triggered.", step);
|
||||
self.to_step(step);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Engine for Tendermint {
|
||||
fn name(&self) -> &str { "Tendermint" }
|
||||
fn version(&self) -> SemanticVersion { SemanticVersion::new(1, 0, 0) }
|
||||
/// (consensus round, proposal signature, authority signatures)
|
||||
fn seal_fields(&self) -> usize { 3 }
|
||||
|
||||
fn params(&self) -> &CommonParams { &self.params }
|
||||
fn builtins(&self) -> &BTreeMap<Address, Builtin> { &self.builtins }
|
||||
|
||||
fn maximum_uncle_count(&self) -> usize { 0 }
|
||||
fn maximum_uncle_age(&self) -> usize { 0 }
|
||||
|
||||
/// Additional engine-specific information for the user/developer concerning `header`.
|
||||
fn extra_info(&self, header: &Header) -> BTreeMap<String, String> {
|
||||
let message = ConsensusMessage::new_proposal(header).expect("Invalid header.");
|
||||
map![
|
||||
"signature".into() => message.signature.to_string(),
|
||||
"height".into() => message.height.to_string(),
|
||||
"round".into() => message.round.to_string(),
|
||||
"block_hash".into() => message.block_hash.as_ref().map(ToString::to_string).unwrap_or("".into())
|
||||
]
|
||||
}
|
||||
|
||||
fn schedule(&self, _env_info: &EnvInfo) -> Schedule {
|
||||
Schedule::new_post_eip150(usize::max_value(), true, true, true)
|
||||
}
|
||||
|
||||
fn populate_from_parent(&self, header: &mut Header, parent: &Header, gas_floor_target: U256, _gas_ceil_target: U256) {
|
||||
header.set_difficulty(parent.difficulty().clone());
|
||||
header.set_gas_limit({
|
||||
let gas_limit = parent.gas_limit().clone();
|
||||
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 - 1.into())
|
||||
} else {
|
||||
max(gas_floor_target, gas_limit - gas_limit / bound_divisor + 1.into())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Should this node participate.
|
||||
fn is_sealer(&self, address: &Address) -> Option<bool> {
|
||||
Some(self.is_authority(address))
|
||||
}
|
||||
|
||||
/// Attempt to seal generate a proposal seal.
|
||||
fn generate_seal(&self, block: &ExecutedBlock) -> Seal {
|
||||
if let Some(ref ap) = *self.account_provider.lock() {
|
||||
let header = block.header();
|
||||
let author = header.author();
|
||||
// Only proposer can generate seal if None was generated.
|
||||
if self.is_proposer(author).is_err() || self.proposal.read().is_some() {
|
||||
return Seal::None;
|
||||
}
|
||||
|
||||
let height = header.number() as Height;
|
||||
let round = self.round.load(AtomicOrdering::SeqCst);
|
||||
let bh = Some(header.bare_hash());
|
||||
let vote_info = message_info_rlp(height, round, Step::Propose, bh.clone());
|
||||
if let Ok(signature) = ap.sign(*author, self.password.read().clone(), vote_info.sha3()).map(H520::from) {
|
||||
// Insert Propose vote.
|
||||
debug!(target: "poa", "Submitting proposal {} at height {} round {}.", header.bare_hash(), height, round);
|
||||
self.votes.vote(ConsensusMessage::new(signature, height, round, Step::Propose, bh), *author);
|
||||
// Remember proposal for later seal submission.
|
||||
*self.proposal.write() = bh;
|
||||
Seal::Proposal(vec![
|
||||
::rlp::encode(&round).to_vec(),
|
||||
::rlp::encode(&signature).to_vec(),
|
||||
::rlp::EMPTY_LIST_RLP.to_vec()
|
||||
])
|
||||
} else {
|
||||
warn!(target: "poa", "generate_seal: FAIL: accounts secret key unavailable");
|
||||
Seal::None
|
||||
}
|
||||
} else {
|
||||
warn!(target: "poa", "generate_seal: FAIL: accounts not provided");
|
||||
Seal::None
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_message(&self, rlp: &[u8]) -> Result<(), Error> {
|
||||
let rlp = UntrustedRlp::new(rlp);
|
||||
let message: ConsensusMessage = try!(rlp.as_val());
|
||||
if !self.votes.is_old_or_known(&message) {
|
||||
let sender = public_to_address(&try!(recover(&message.signature.into(), &try!(rlp.at(1)).as_raw().sha3())));
|
||||
if !self.is_authority(&sender) {
|
||||
try!(Err(EngineError::NotAuthorized(sender)));
|
||||
}
|
||||
self.broadcast_message(rlp.as_raw().to_vec());
|
||||
trace!(target: "poa", "Handling a valid {:?} from {}.", message, sender);
|
||||
self.votes.vote(message.clone(), sender);
|
||||
self.handle_valid_message(&message);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_block_basic(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
||||
let seal_length = header.seal().len();
|
||||
if seal_length == self.seal_fields() {
|
||||
let signatures_len = header.seal()[2].len();
|
||||
if signatures_len >= 1 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(From::from(EngineError::BadSealFieldSize(OutOfBounds {
|
||||
min: Some(1),
|
||||
max: None,
|
||||
found: signatures_len
|
||||
})))
|
||||
}
|
||||
} else {
|
||||
Err(From::from(BlockError::InvalidSealArity(
|
||||
Mismatch { expected: self.seal_fields(), found: seal_length }
|
||||
)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn verify_block_unordered(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
||||
let proposal = try!(ConsensusMessage::new_proposal(header));
|
||||
let proposer = try!(proposal.verify());
|
||||
if !self.is_authority(&proposer) {
|
||||
try!(Err(EngineError::NotAuthorized(proposer)))
|
||||
}
|
||||
|
||||
let precommit_hash = proposal.precommit_hash();
|
||||
let ref signatures_field = header.seal()[2];
|
||||
let mut signature_count = 0;
|
||||
let mut origins = HashSet::new();
|
||||
for rlp in UntrustedRlp::new(signatures_field).iter() {
|
||||
let precommit: ConsensusMessage = ConsensusMessage::new_commit(&proposal, try!(rlp.as_val()));
|
||||
let address = match self.votes.get(&precommit) {
|
||||
Some(a) => a,
|
||||
None => public_to_address(&try!(recover(&precommit.signature.into(), &precommit_hash))),
|
||||
};
|
||||
if !self.our_params.authorities.contains(&address) {
|
||||
try!(Err(EngineError::NotAuthorized(address.to_owned())))
|
||||
}
|
||||
|
||||
if origins.insert(address) {
|
||||
signature_count += 1;
|
||||
} else {
|
||||
warn!(target: "poa", "verify_block_unordered: Duplicate signature from {} on the seal.", address);
|
||||
try!(Err(BlockError::InvalidSeal));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if its a proposal if there is not enough precommits.
|
||||
if !self.is_above_threshold(signature_count) {
|
||||
let signatures_len = signatures_field.len();
|
||||
// Proposal has to have an empty signature list.
|
||||
if signatures_len != 1 {
|
||||
try!(Err(EngineError::BadSealFieldSize(OutOfBounds {
|
||||
min: Some(1),
|
||||
max: Some(1),
|
||||
found: signatures_len
|
||||
})));
|
||||
}
|
||||
try!(self.is_round_proposer(proposal.height, proposal.round, &proposer));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
||||
if header.number() == 0 {
|
||||
try!(Err(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() })));
|
||||
}
|
||||
|
||||
let gas_limit_divisor = self.our_params.gas_limit_bound_divisor;
|
||||
let min_gas = parent.gas_limit().clone() - parent.gas_limit().clone() / gas_limit_divisor;
|
||||
let max_gas = parent.gas_limit().clone() + parent.gas_limit().clone() / gas_limit_divisor;
|
||||
if header.gas_limit() <= &min_gas || header.gas_limit() >= &max_gas {
|
||||
try!(Err(BlockError::InvalidGasLimit(OutOfBounds { min: Some(min_gas), max: Some(max_gas), found: header.gas_limit().clone() })));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_transaction_basic(&self, t: &SignedTransaction, _header: &Header) -> 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
|
||||
}
|
||||
|
||||
fn set_signer(&self, address: Address, password: String) {
|
||||
*self.authority.write() = address;
|
||||
*self.password.write() = Some(password);
|
||||
self.to_step(Step::Propose);
|
||||
}
|
||||
|
||||
fn stop(&self) {
|
||||
self.step_service.stop()
|
||||
}
|
||||
|
||||
fn is_new_best_block(&self, _best_total_difficulty: U256, best_header: HeaderView, _parent_details: &BlockDetails, new_header: &HeaderView) -> bool {
|
||||
let new_number = new_header.number();
|
||||
let best_number = best_header.number();
|
||||
trace!(target: "poa", "new_header: {}, best_header: {}", new_number, best_number);
|
||||
if new_number != best_number {
|
||||
new_number > best_number
|
||||
} else {
|
||||
let new_seal = new_header.seal();
|
||||
let best_seal = best_header.seal();
|
||||
let new_signatures = new_seal.get(2).expect("Tendermint seal should have three elements.").len();
|
||||
let best_signatures = best_seal.get(2).expect("Tendermint seal should have three elements.").len();
|
||||
if new_signatures > best_signatures {
|
||||
true
|
||||
} else {
|
||||
let new_round: Round = ::rlp::Rlp::new(&new_seal.get(0).expect("Tendermint seal should have three elements.")).as_val();
|
||||
let best_round: Round = ::rlp::Rlp::new(&best_seal.get(0).expect("Tendermint seal should have three elements.")).as_val();
|
||||
new_round > best_round
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_proposal(&self, header: &Header) -> bool {
|
||||
let signatures_len = header.seal()[2].len();
|
||||
// Signatures have to be an empty list rlp.
|
||||
let proposal = ConsensusMessage::new_proposal(header).expect("block went through full verification; this Engine verifies new_proposal creation; qed");
|
||||
if signatures_len != 1 {
|
||||
// New Commit received, skip to next height.
|
||||
trace!(target: "poa", "Received a commit for height {}, round {}.", proposal.height, proposal.round);
|
||||
self.to_next_height(proposal.height);
|
||||
return false;
|
||||
}
|
||||
let proposer = proposal.verify().expect("block went through full verification; this Engine tries verify; qed");
|
||||
debug!(target: "poa", "Received a new proposal for height {}, round {} from {}.", proposal.height, proposal.round, proposer);
|
||||
if self.is_round(&proposal) {
|
||||
*self.proposal.write() = proposal.block_hash.clone();
|
||||
}
|
||||
self.votes.vote(proposal, proposer);
|
||||
true
|
||||
}
|
||||
|
||||
/// Equivalent to a timeout: to be used for tests.
|
||||
fn step(&self) {
|
||||
let next_step = match *self.step.read() {
|
||||
Step::Propose => {
|
||||
trace!(target: "poa", "Propose timeout.");
|
||||
Step::Prevote
|
||||
},
|
||||
Step::Prevote if self.has_enough_any_votes() => {
|
||||
trace!(target: "poa", "Prevote timeout.");
|
||||
Step::Precommit
|
||||
},
|
||||
Step::Prevote => {
|
||||
trace!(target: "poa", "Prevote timeout without enough votes.");
|
||||
self.broadcast_old_messages();
|
||||
Step::Prevote
|
||||
},
|
||||
Step::Precommit if self.has_enough_any_votes() => {
|
||||
trace!(target: "poa", "Precommit timeout.");
|
||||
self.increment_round(1);
|
||||
Step::Propose
|
||||
},
|
||||
Step::Precommit => {
|
||||
trace!(target: "poa", "Precommit timeout without enough votes.");
|
||||
self.broadcast_old_messages();
|
||||
Step::Precommit
|
||||
},
|
||||
Step::Commit => {
|
||||
trace!(target: "poa", "Commit timeout.");
|
||||
Step::Propose
|
||||
},
|
||||
};
|
||||
self.to_step(next_step);
|
||||
}
|
||||
|
||||
fn register_message_channel(&self, message_channel: IoChannel<ClientIoMessage>) {
|
||||
trace!(target: "poa", "Register the IoChannel.");
|
||||
*self.message_channel.lock() = Some(message_channel);
|
||||
}
|
||||
|
||||
fn register_account_provider(&self, account_provider: Arc<AccountProvider>) {
|
||||
*self.account_provider.lock() = Some(account_provider);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use util::*;
|
||||
use util::trie::TrieSpec;
|
||||
use io::{IoContext, IoHandler};
|
||||
use block::*;
|
||||
use error::{Error, BlockError};
|
||||
use header::Header;
|
||||
use io::IoChannel;
|
||||
use env_info::EnvInfo;
|
||||
use tests::helpers::*;
|
||||
use account_provider::AccountProvider;
|
||||
use service::ClientIoMessage;
|
||||
use spec::Spec;
|
||||
use engines::{Engine, EngineError, Seal};
|
||||
use super::*;
|
||||
use super::message::*;
|
||||
|
||||
/// Accounts inserted with "0" and "1" are authorities. First proposer is "0".
|
||||
fn setup() -> (Spec, Arc<AccountProvider>) {
|
||||
let tap = Arc::new(AccountProvider::transient_provider());
|
||||
let spec = Spec::new_test_tendermint();
|
||||
spec.engine.register_account_provider(tap.clone());
|
||||
(spec, tap)
|
||||
}
|
||||
|
||||
fn propose_default(spec: &Spec, proposer: Address) -> (LockedBlock, Vec<Bytes>) {
|
||||
let mut db_result = get_temp_state_db();
|
||||
let mut db = db_result.take();
|
||||
spec.ensure_db_good(&mut db, &TrieFactory::new(TrieSpec::Secure)).unwrap();
|
||||
let genesis_header = spec.genesis_header();
|
||||
let last_hashes = Arc::new(vec![genesis_header.hash()]);
|
||||
let b = OpenBlock::new(spec.engine.as_ref(), Default::default(), false, db.boxed_clone(), &genesis_header, last_hashes, proposer, (3141562.into(), 31415620.into()), vec![]).unwrap();
|
||||
let b = b.close_and_lock();
|
||||
if let Seal::Proposal(seal) = spec.engine.generate_seal(b.block()) {
|
||||
(b, seal)
|
||||
} else {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
|
||||
fn vote<F>(engine: &Arc<Engine>, signer: F, height: usize, round: usize, step: Step, block_hash: Option<H256>) -> Bytes where F: FnOnce(H256) -> Result<H520, ::account_provider::Error> {
|
||||
let mi = message_info_rlp(height, round, step, block_hash);
|
||||
let m = message_full_rlp(&signer(mi.sha3()).unwrap().into(), &mi);
|
||||
engine.handle_message(&m).unwrap();
|
||||
m
|
||||
}
|
||||
|
||||
fn proposal_seal(tap: &Arc<AccountProvider>, header: &Header, round: Round) -> Vec<Bytes> {
|
||||
let author = header.author();
|
||||
let vote_info = message_info_rlp(header.number() as Height, round, Step::Propose, Some(header.bare_hash()));
|
||||
let signature = tap.sign(*author, None, vote_info.sha3()).unwrap();
|
||||
vec![
|
||||
::rlp::encode(&round).to_vec(),
|
||||
::rlp::encode(&H520::from(signature)).to_vec(),
|
||||
::rlp::EMPTY_LIST_RLP.to_vec()
|
||||
]
|
||||
}
|
||||
|
||||
fn precommit_signatures(tap: &Arc<AccountProvider>, height: Height, round: Round, bare_hash: Option<H256>, v1: H160, v2: H160) -> Bytes {
|
||||
let vote_info = message_info_rlp(height, round, Step::Precommit, bare_hash);
|
||||
::rlp::encode(&vec![
|
||||
H520::from(tap.sign(v1, None, vote_info.sha3()).unwrap()),
|
||||
H520::from(tap.sign(v2, None, vote_info.sha3()).unwrap())
|
||||
]).to_vec()
|
||||
}
|
||||
|
||||
fn insert_and_unlock(tap: &Arc<AccountProvider>, acc: &str) -> Address {
|
||||
let addr = tap.insert_account(acc.sha3(), acc).unwrap();
|
||||
tap.unlock_account_permanently(addr, acc.into()).unwrap();
|
||||
addr
|
||||
}
|
||||
|
||||
fn insert_and_register(tap: &Arc<AccountProvider>, engine: &Arc<Engine>, acc: &str) -> Address {
|
||||
let addr = insert_and_unlock(tap, acc);
|
||||
engine.set_signer(addr.clone(), acc.into());
|
||||
addr
|
||||
}
|
||||
|
||||
struct TestIo {
|
||||
received: RwLock<Vec<ClientIoMessage>>
|
||||
}
|
||||
|
||||
impl TestIo {
|
||||
fn new() -> Arc<Self> { Arc::new(TestIo { received: RwLock::new(Vec::new()) }) }
|
||||
}
|
||||
|
||||
impl IoHandler<ClientIoMessage> for TestIo {
|
||||
fn message(&self, _io: &IoContext<ClientIoMessage>, net_message: &ClientIoMessage) {
|
||||
self.received.write().push(net_message.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_valid_metadata() {
|
||||
let engine = Spec::new_test_tendermint().engine;
|
||||
assert!(!engine.name().is_empty());
|
||||
assert!(engine.version().major >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_return_schedule() {
|
||||
let engine = Spec::new_test_tendermint().engine;
|
||||
let schedule = engine.schedule(&EnvInfo {
|
||||
number: 10000000,
|
||||
author: 0.into(),
|
||||
timestamp: 0,
|
||||
difficulty: 0.into(),
|
||||
last_hashes: Arc::new(vec![]),
|
||||
gas_used: 0.into(),
|
||||
gas_limit: 0.into(),
|
||||
});
|
||||
|
||||
assert!(schedule.stack_limit > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_fails_on_short_seal() {
|
||||
let engine = Spec::new_test_tendermint().engine;
|
||||
let 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 allows_correct_proposer() {
|
||||
let (spec, tap) = setup();
|
||||
let engine = spec.engine;
|
||||
|
||||
let mut header = Header::default();
|
||||
let validator = insert_and_unlock(&tap, "0");
|
||||
header.set_author(validator);
|
||||
let seal = proposal_seal(&tap, &header, 0);
|
||||
header.set_seal(seal);
|
||||
// Good proposer.
|
||||
assert!(engine.verify_block_unordered(&header.clone(), None).is_ok());
|
||||
|
||||
let validator = insert_and_unlock(&tap, "1");
|
||||
header.set_author(validator);
|
||||
let seal = proposal_seal(&tap, &header, 0);
|
||||
header.set_seal(seal);
|
||||
// Bad proposer.
|
||||
match engine.verify_block_unordered(&header, None) {
|
||||
Err(Error::Engine(EngineError::NotProposer(_))) => {},
|
||||
_ => panic!(),
|
||||
}
|
||||
|
||||
let random = insert_and_unlock(&tap, "101");
|
||||
header.set_author(random);
|
||||
let seal = proposal_seal(&tap, &header, 0);
|
||||
header.set_seal(seal);
|
||||
// Not authority.
|
||||
match engine.verify_block_unordered(&header, None) {
|
||||
Err(Error::Engine(EngineError::NotAuthorized(_))) => {},
|
||||
_ => panic!(),
|
||||
};
|
||||
engine.stop();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seal_signatures_checking() {
|
||||
let (spec, tap) = setup();
|
||||
let engine = spec.engine;
|
||||
|
||||
let mut header = Header::default();
|
||||
let proposer = insert_and_unlock(&tap, "1");
|
||||
header.set_author(proposer);
|
||||
let mut seal = proposal_seal(&tap, &header, 0);
|
||||
|
||||
let vote_info = message_info_rlp(0, 0, Step::Precommit, Some(header.bare_hash()));
|
||||
let signature1 = tap.sign(proposer, None, vote_info.sha3()).unwrap();
|
||||
|
||||
seal[2] = ::rlp::encode(&vec![H520::from(signature1.clone())]).to_vec();
|
||||
header.set_seal(seal.clone());
|
||||
|
||||
// One good signature is not enough.
|
||||
match engine.verify_block_unordered(&header, None) {
|
||||
Err(Error::Engine(EngineError::BadSealFieldSize(_))) => {},
|
||||
_ => panic!(),
|
||||
}
|
||||
|
||||
let voter = insert_and_unlock(&tap, "0");
|
||||
let signature0 = tap.sign(voter, None, vote_info.sha3()).unwrap();
|
||||
|
||||
seal[2] = ::rlp::encode(&vec![H520::from(signature1.clone()), H520::from(signature0.clone())]).to_vec();
|
||||
header.set_seal(seal.clone());
|
||||
|
||||
assert!(engine.verify_block_unordered(&header, None).is_ok());
|
||||
|
||||
let bad_voter = insert_and_unlock(&tap, "101");
|
||||
let bad_signature = tap.sign(bad_voter, None, vote_info.sha3()).unwrap();
|
||||
|
||||
seal[2] = ::rlp::encode(&vec![H520::from(signature1), H520::from(bad_signature)]).to_vec();
|
||||
header.set_seal(seal);
|
||||
|
||||
// One good and one bad signature.
|
||||
match engine.verify_block_unordered(&header, None) {
|
||||
Err(Error::Engine(EngineError::NotAuthorized(_))) => {},
|
||||
_ => panic!(),
|
||||
};
|
||||
engine.stop();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_generate_seal() {
|
||||
let (spec, tap) = setup();
|
||||
|
||||
let proposer = insert_and_register(&tap, &spec.engine, "1");
|
||||
|
||||
let (b, seal) = propose_default(&spec, proposer);
|
||||
assert!(b.try_seal(spec.engine.as_ref(), seal).is_ok());
|
||||
spec.engine.stop();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_recognize_proposal() {
|
||||
let (spec, tap) = setup();
|
||||
|
||||
let proposer = insert_and_register(&tap, &spec.engine, "1");
|
||||
|
||||
let (b, seal) = propose_default(&spec, proposer);
|
||||
let sealed = b.seal(spec.engine.as_ref(), seal).unwrap();
|
||||
assert!(spec.engine.is_proposal(sealed.header()));
|
||||
spec.engine.stop();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relays_messages() {
|
||||
let (spec, tap) = setup();
|
||||
let engine = spec.engine.clone();
|
||||
let mut db_result = get_temp_state_db();
|
||||
let mut db = db_result.take();
|
||||
spec.ensure_db_good(&mut db, &TrieFactory::new(TrieSpec::Secure)).unwrap();
|
||||
|
||||
let v0 = insert_and_register(&tap, &engine, "0");
|
||||
let v1 = insert_and_register(&tap, &engine, "1");
|
||||
|
||||
let h = 0;
|
||||
let r = 0;
|
||||
|
||||
// Propose
|
||||
let (b, _) = propose_default(&spec, v1.clone());
|
||||
let proposal = Some(b.header().bare_hash());
|
||||
|
||||
// Register IoHandler remembers messages.
|
||||
let test_io = TestIo::new();
|
||||
let channel = IoChannel::to_handler(Arc::downgrade(&(test_io.clone() as Arc<IoHandler<ClientIoMessage>>)));
|
||||
engine.register_message_channel(channel);
|
||||
|
||||
let prevote_current = vote(&engine, |mh| tap.sign(v0, None, mh).map(H520::from), h, r, Step::Prevote, proposal);
|
||||
|
||||
let precommit_current = vote(&engine, |mh| tap.sign(v0, None, mh).map(H520::from), h, r, Step::Precommit, proposal);
|
||||
|
||||
let prevote_future = vote(&engine, |mh| tap.sign(v0, None, mh).map(H520::from), h + 1, r, Step::Prevote, proposal);
|
||||
|
||||
// Relays all valid present and future messages.
|
||||
assert!(test_io.received.read().contains(&ClientIoMessage::BroadcastMessage(prevote_current)));
|
||||
assert!(test_io.received.read().contains(&ClientIoMessage::BroadcastMessage(precommit_current)));
|
||||
assert!(test_io.received.read().contains(&ClientIoMessage::BroadcastMessage(prevote_future)));
|
||||
engine.stop();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seal_submission() {
|
||||
let (spec, tap) = setup();
|
||||
let engine = spec.engine.clone();
|
||||
let mut db_result = get_temp_state_db();
|
||||
let mut db = db_result.take();
|
||||
spec.ensure_db_good(&mut db, &TrieFactory::new(TrieSpec::Secure)).unwrap();
|
||||
|
||||
let v0 = insert_and_register(&tap, &engine, "0");
|
||||
let v1 = insert_and_register(&tap, &engine, "1");
|
||||
|
||||
let h = 1;
|
||||
let r = 0;
|
||||
|
||||
// Register IoHandler remembers messages.
|
||||
let test_io = TestIo::new();
|
||||
let channel = IoChannel::to_handler(Arc::downgrade(&(test_io.clone() as Arc<IoHandler<ClientIoMessage>>)));
|
||||
engine.register_message_channel(channel);
|
||||
|
||||
// Propose
|
||||
let (b, mut seal) = propose_default(&spec, v1.clone());
|
||||
let proposal = Some(b.header().bare_hash());
|
||||
engine.step();
|
||||
|
||||
// Prevote.
|
||||
vote(&engine, |mh| tap.sign(v1, None, mh).map(H520::from), h, r, Step::Prevote, proposal);
|
||||
vote(&engine, |mh| tap.sign(v0, None, mh).map(H520::from), h, r, Step::Prevote, proposal);
|
||||
vote(&engine, |mh| tap.sign(v1, None, mh).map(H520::from), h, r, Step::Precommit, proposal);
|
||||
vote(&engine, |mh| tap.sign(v0, None, mh).map(H520::from), h, r, Step::Precommit, proposal);
|
||||
|
||||
seal[2] = precommit_signatures(&tap, h, r, Some(b.header().bare_hash()), v1, v0);
|
||||
let first = test_io.received.read().contains(&ClientIoMessage::SubmitSeal(proposal.unwrap(), seal.clone()));
|
||||
seal[2] = precommit_signatures(&tap, h, r, Some(b.header().bare_hash()), v0, v1);
|
||||
let second = test_io.received.read().contains(&ClientIoMessage::SubmitSeal(proposal.unwrap(), seal));
|
||||
|
||||
assert!(first ^ second);
|
||||
engine.stop();
|
||||
}
|
||||
}
|
||||
72
ethcore/src/engines/tendermint/params.rs
Normal file
72
ethcore/src/engines/tendermint/params.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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/>.
|
||||
|
||||
//! Tendermint specific parameters.
|
||||
|
||||
use ethjson;
|
||||
use super::transition::TendermintTimeouts;
|
||||
use util::{Address, U256};
|
||||
use time::Duration;
|
||||
|
||||
/// `Tendermint` params.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TendermintParams {
|
||||
/// Gas limit divisor.
|
||||
pub gas_limit_bound_divisor: U256,
|
||||
/// List of authorities.
|
||||
pub authorities: Vec<Address>,
|
||||
/// Number of authorities.
|
||||
pub authority_n: usize,
|
||||
/// Timeout durations for different steps.
|
||||
pub timeouts: TendermintTimeouts,
|
||||
}
|
||||
|
||||
impl Default for TendermintParams {
|
||||
fn default() -> Self {
|
||||
let authorities = vec!["0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e".into(), "0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1".into()];
|
||||
let val_n = authorities.len();
|
||||
TendermintParams {
|
||||
gas_limit_bound_divisor: 0x0400.into(),
|
||||
authorities: authorities,
|
||||
authority_n: val_n,
|
||||
timeouts: TendermintTimeouts::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_duration(ms: ethjson::uint::Uint) -> Duration {
|
||||
let ms: usize = ms.into();
|
||||
Duration::milliseconds(ms as i64)
|
||||
}
|
||||
|
||||
impl From<ethjson::spec::TendermintParams> for TendermintParams {
|
||||
fn from(p: ethjson::spec::TendermintParams) -> Self {
|
||||
let val: Vec<_> = p.authorities.into_iter().map(Into::into).collect();
|
||||
let val_n = val.len();
|
||||
let dt = TendermintTimeouts::default();
|
||||
TendermintParams {
|
||||
gas_limit_bound_divisor: p.gas_limit_bound_divisor.into(),
|
||||
authorities: val,
|
||||
authority_n: val_n,
|
||||
timeouts: TendermintTimeouts {
|
||||
propose: p.timeout_propose.map_or(dt.propose, to_duration),
|
||||
prevote: p.timeout_prevote.map_or(dt.prevote, to_duration),
|
||||
precommit: p.timeout_precommit.map_or(dt.precommit, to_duration),
|
||||
commit: p.timeout_commit.map_or(dt.commit, to_duration),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
96
ethcore/src/engines/tendermint/transition.rs
Normal file
96
ethcore/src/engines/tendermint/transition.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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/>.
|
||||
|
||||
//! Tendermint timeout handling.
|
||||
|
||||
use std::sync::Weak;
|
||||
use time::Duration;
|
||||
use io::{IoContext, IoHandler, TimerToken};
|
||||
use super::{Tendermint, Step};
|
||||
use engines::Engine;
|
||||
|
||||
pub struct TransitionHandler {
|
||||
pub engine: Weak<Tendermint>,
|
||||
}
|
||||
|
||||
/// Base timeout of each step in ms.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TendermintTimeouts {
|
||||
pub propose: Duration,
|
||||
pub prevote: Duration,
|
||||
pub precommit: Duration,
|
||||
pub commit: Duration,
|
||||
}
|
||||
|
||||
impl TendermintTimeouts {
|
||||
pub fn for_step(&self, step: Step) -> Duration {
|
||||
match step {
|
||||
Step::Propose => self.propose,
|
||||
Step::Prevote => self.prevote,
|
||||
Step::Precommit => self.precommit,
|
||||
Step::Commit => self.commit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TendermintTimeouts {
|
||||
fn default() -> Self {
|
||||
TendermintTimeouts {
|
||||
propose: Duration::milliseconds(10000),
|
||||
prevote: Duration::milliseconds(10000),
|
||||
precommit: Duration::milliseconds(10000),
|
||||
commit: Duration::milliseconds(10000),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Timer token representing the consensus step timeouts.
|
||||
pub const ENGINE_TIMEOUT_TOKEN: TimerToken = 23;
|
||||
|
||||
fn set_timeout(io: &IoContext<Step>, timeout: Duration) {
|
||||
io.register_timer_once(ENGINE_TIMEOUT_TOKEN, timeout.num_milliseconds() as u64)
|
||||
.unwrap_or_else(|e| warn!(target: "poa", "Failed to set consensus step timeout: {}.", e))
|
||||
}
|
||||
|
||||
impl IoHandler<Step> for TransitionHandler {
|
||||
fn initialize(&self, io: &IoContext<Step>) {
|
||||
if let Some(engine) = self.engine.upgrade() {
|
||||
set_timeout(io, engine.our_params.timeouts.propose)
|
||||
}
|
||||
}
|
||||
|
||||
fn timeout(&self, _io: &IoContext<Step>, timer: TimerToken) {
|
||||
if timer == ENGINE_TIMEOUT_TOKEN {
|
||||
if let Some(engine) = self.engine.upgrade() {
|
||||
engine.step();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn message(&self, io: &IoContext<Step>, next_step: &Step) {
|
||||
if let Some(engine) = self.engine.upgrade() {
|
||||
if let Err(io_err) = io.clear_timer(ENGINE_TIMEOUT_TOKEN) {
|
||||
warn!(target: "poa", "Could not remove consensus timer {}.", io_err)
|
||||
}
|
||||
match *next_step {
|
||||
Step::Propose => set_timeout(io, engine.our_params.timeouts.propose),
|
||||
Step::Prevote => set_timeout(io, engine.our_params.timeouts.prevote),
|
||||
Step::Precommit => set_timeout(io, engine.our_params.timeouts.precommit),
|
||||
Step::Commit => set_timeout(io, engine.our_params.timeouts.commit),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
272
ethcore/src/engines/tendermint/vote_collector.rs
Normal file
272
ethcore/src/engines/tendermint/vote_collector.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
// 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/>.
|
||||
|
||||
//! Collects votes on hashes at each height and round.
|
||||
|
||||
use util::*;
|
||||
use super::message::ConsensusMessage;
|
||||
use super::{Height, Round, Step};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VoteCollector {
|
||||
/// Storing all Proposals, Prevotes and Precommits.
|
||||
votes: RwLock<BTreeMap<ConsensusMessage, Address>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SealSignatures {
|
||||
pub proposal: H520,
|
||||
pub votes: Vec<H520>,
|
||||
}
|
||||
|
||||
impl PartialEq for SealSignatures {
|
||||
fn eq(&self, other: &SealSignatures) -> bool {
|
||||
self.proposal == other.proposal
|
||||
&& self.votes.iter().collect::<HashSet<_>>() == other.votes.iter().collect::<HashSet<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for SealSignatures {}
|
||||
|
||||
impl VoteCollector {
|
||||
pub fn new() -> VoteCollector {
|
||||
let mut collector = BTreeMap::new();
|
||||
// Insert dummy message to fulfill invariant: "only messages newer than the oldest are inserted".
|
||||
collector.insert(ConsensusMessage {
|
||||
signature: H520::default(),
|
||||
height: 0,
|
||||
round: 0,
|
||||
step: Step::Propose,
|
||||
block_hash: None
|
||||
},
|
||||
Address::default());
|
||||
VoteCollector { votes: RwLock::new(collector) }
|
||||
}
|
||||
|
||||
/// Insert vote if it is newer than the oldest one.
|
||||
pub fn vote(&self, message: ConsensusMessage, voter: Address) -> Option<Address> {
|
||||
self.votes.write().insert(message, voter)
|
||||
}
|
||||
|
||||
pub fn is_old_or_known(&self, message: &ConsensusMessage) -> bool {
|
||||
self.votes.read().get(message).map_or(false, |a| {
|
||||
trace!(target: "poa", "Known message from {}: {:?}.", a, message);
|
||||
true
|
||||
}) || {
|
||||
let guard = self.votes.read();
|
||||
let is_old = guard.keys().next().map_or(true, |oldest| message <= oldest);
|
||||
if is_old { trace!(target: "poa", "Old message {:?}.", message); }
|
||||
is_old
|
||||
}
|
||||
}
|
||||
|
||||
/// Throws out messages older than message, leaves message as marker for the oldest.
|
||||
pub fn throw_out_old(&self, message: &ConsensusMessage) {
|
||||
let mut guard = self.votes.write();
|
||||
let new_collector = guard.split_off(message);
|
||||
*guard = new_collector;
|
||||
}
|
||||
|
||||
pub fn seal_signatures(&self, height: Height, round: Round, block_hash: H256) -> Option<SealSignatures> {
|
||||
let bh = Some(block_hash);
|
||||
let (proposal, votes) = {
|
||||
let guard = self.votes.read();
|
||||
let mut current_signatures = guard.keys().skip_while(|m| !m.is_block_hash(height, round, Step::Propose, bh));
|
||||
let proposal = current_signatures.next().cloned();
|
||||
let votes = current_signatures
|
||||
.skip_while(|m| !m.is_block_hash(height, round, Step::Precommit, bh))
|
||||
.filter(|m| m.is_block_hash(height, round, Step::Precommit, bh))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
(proposal, votes)
|
||||
};
|
||||
if votes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Remove messages that are no longer relevant.
|
||||
votes.last().map(|m| self.throw_out_old(m));
|
||||
let mut votes_vec: Vec<_> = votes.into_iter().map(|m| m.signature).collect();
|
||||
votes_vec.sort();
|
||||
proposal.map(|p| SealSignatures {
|
||||
proposal: p.signature,
|
||||
votes: votes_vec,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn count_aligned_votes(&self, message: &ConsensusMessage) -> usize {
|
||||
let guard = self.votes.read();
|
||||
guard.keys()
|
||||
.skip_while(|m| !m.is_aligned(message))
|
||||
// sorted by signature so might not be continuous
|
||||
.filter(|m| m.is_aligned(message))
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn count_step_votes(&self, height: Height, round: Round, step: Step) -> usize {
|
||||
let guard = self.votes.read();
|
||||
let current = guard.iter().skip_while(|&(m, _)| !m.is_step(height, round, step));
|
||||
let mut origins = HashSet::new();
|
||||
let mut n = 0;
|
||||
for (message, origin) in current {
|
||||
if message.is_step(height, round, step) {
|
||||
if origins.insert(origin) {
|
||||
n += 1;
|
||||
} else {
|
||||
warn!("count_step_votes: Authority {} has cast multiple step votes, this indicates malicious behaviour.", origin)
|
||||
}
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
pub fn get_up_to(&self, height: Height) -> Vec<Bytes> {
|
||||
let guard = self.votes.read();
|
||||
guard
|
||||
.keys()
|
||||
.filter(|m| m.step.is_pre())
|
||||
.take_while(|m| m.height <= height)
|
||||
.map(|m| ::rlp::encode(m).to_vec())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get(&self, message: &ConsensusMessage) -> Option<Address> {
|
||||
let guard = self.votes.read();
|
||||
guard.get(message).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use util::*;
|
||||
use super::*;
|
||||
use super::super::{Height, Round, BlockHash, Step};
|
||||
use super::super::message::ConsensusMessage;
|
||||
|
||||
fn random_vote(collector: &VoteCollector, signature: H520, h: Height, r: Round, step: Step, block_hash: Option<BlockHash>) -> Option<H160> {
|
||||
full_vote(collector, signature, h, r, step, block_hash, H160::random())
|
||||
}
|
||||
|
||||
fn full_vote(collector: &VoteCollector, signature: H520, h: Height, r: Round, step: Step, block_hash: Option<BlockHash>, address: Address) -> Option<H160> {
|
||||
collector.vote(ConsensusMessage { signature: signature, height: h, round: r, step: step, block_hash: block_hash }, address)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seal_retrieval() {
|
||||
let collector = VoteCollector::new();
|
||||
let bh = Some("1".sha3());
|
||||
let h = 1;
|
||||
let r = 2;
|
||||
let mut signatures = Vec::new();
|
||||
for _ in 0..5 {
|
||||
signatures.push(H520::random());
|
||||
}
|
||||
// Wrong height proposal.
|
||||
random_vote(&collector, signatures[4].clone(), h - 1, r, Step::Propose, bh.clone());
|
||||
// Good proposal
|
||||
random_vote(&collector, signatures[0].clone(), h, r, Step::Propose, bh.clone());
|
||||
// Wrong block proposal.
|
||||
random_vote(&collector, signatures[0].clone(), h, r, Step::Propose, Some("0".sha3()));
|
||||
// Wrong block precommit.
|
||||
random_vote(&collector, signatures[3].clone(), h, r, Step::Precommit, Some("0".sha3()));
|
||||
// Wrong round proposal.
|
||||
random_vote(&collector, signatures[0].clone(), h, r - 1, Step::Propose, bh.clone());
|
||||
// Prevote.
|
||||
random_vote(&collector, signatures[0].clone(), h, r, Step::Prevote, bh.clone());
|
||||
// Relevant precommit.
|
||||
random_vote(&collector, signatures[2].clone(), h, r, Step::Precommit, bh.clone());
|
||||
// Replcated vote.
|
||||
random_vote(&collector, signatures[2].clone(), h, r, Step::Precommit, bh.clone());
|
||||
// Wrong round precommit.
|
||||
random_vote(&collector, signatures[4].clone(), h, r + 1, Step::Precommit, bh.clone());
|
||||
// Wrong height precommit.
|
||||
random_vote(&collector, signatures[3].clone(), h + 1, r, Step::Precommit, bh.clone());
|
||||
// Relevant precommit.
|
||||
random_vote(&collector, signatures[1].clone(), h, r, Step::Precommit, bh.clone());
|
||||
// Wrong round precommit, same signature.
|
||||
random_vote(&collector, signatures[1].clone(), h, r + 1, Step::Precommit, bh.clone());
|
||||
// Wrong round precommit.
|
||||
random_vote(&collector, signatures[4].clone(), h, r - 1, Step::Precommit, bh.clone());
|
||||
let seal = SealSignatures {
|
||||
proposal: signatures[0],
|
||||
votes: signatures[1..3].to_vec()
|
||||
};
|
||||
assert_eq!(seal, collector.seal_signatures(h, r, bh.unwrap()).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_votes() {
|
||||
let collector = VoteCollector::new();
|
||||
// good prevote
|
||||
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), 3, 1, Step::Prevote, Some("0".sha3()));
|
||||
// good precommit
|
||||
random_vote(&collector, H520::random(), 3, 2, Step::Precommit, Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), 3, 3, Step::Precommit, Some("0".sha3()));
|
||||
// good prevote
|
||||
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()));
|
||||
// good prevote
|
||||
let same_sig = H520::random();
|
||||
random_vote(&collector, same_sig.clone(), 3, 2, Step::Prevote, Some("1".sha3()));
|
||||
random_vote(&collector, same_sig, 3, 2, Step::Prevote, Some("1".sha3()));
|
||||
// good precommit
|
||||
random_vote(&collector, H520::random(), 3, 2, Step::Precommit, Some("1".sha3()));
|
||||
// good prevote
|
||||
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), 2, 2, Step::Precommit, Some("2".sha3()));
|
||||
|
||||
assert_eq!(collector.count_step_votes(3, 2, Step::Prevote), 4);
|
||||
assert_eq!(collector.count_step_votes(3, 2, Step::Precommit), 2);
|
||||
|
||||
let message = ConsensusMessage {
|
||||
signature: H520::default(),
|
||||
height: 3,
|
||||
round: 2,
|
||||
step: Step::Prevote,
|
||||
block_hash: Some("1".sha3())
|
||||
};
|
||||
assert_eq!(collector.count_aligned_votes(&message), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_old() {
|
||||
let collector = VoteCollector::new();
|
||||
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), 3, 1, Step::Prevote, Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), 3, 3, Step::Precommit, Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()));
|
||||
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()));
|
||||
random_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()));
|
||||
random_vote(&collector, H520::random(), 2, 2, Step::Precommit, Some("2".sha3()));
|
||||
|
||||
let message = ConsensusMessage {
|
||||
signature: H520::default(),
|
||||
height: 3,
|
||||
round: 2,
|
||||
step: Step::Precommit,
|
||||
block_hash: Some("1".sha3())
|
||||
};
|
||||
collector.throw_out_old(&message);
|
||||
assert_eq!(collector.votes.read().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malicious_authority() {
|
||||
let collector = VoteCollector::new();
|
||||
full_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("0".sha3()), Address::default());
|
||||
full_vote(&collector, H520::random(), 3, 2, Step::Prevote, Some("1".sha3()), Address::default());
|
||||
assert_eq!(collector.count_step_votes(3, 2, Step::Prevote), 1);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ use client::Error as ClientError;
|
||||
use ipc::binary::{BinaryConvertError, BinaryConvertable};
|
||||
use types::block_import_error::BlockImportError;
|
||||
use snapshot::Error as SnapshotError;
|
||||
use engines::EngineError;
|
||||
use ethkey::Error as EthkeyError;
|
||||
|
||||
pub use types::executed::{ExecutionError, CallError};
|
||||
@@ -167,8 +168,6 @@ pub enum BlockError {
|
||||
UnknownParent(H256),
|
||||
/// Uncle parent given is unknown.
|
||||
UnknownUncleParent(H256),
|
||||
/// The same author issued different votes at the same step.
|
||||
DoubleVote(H160),
|
||||
}
|
||||
|
||||
impl fmt::Display for BlockError {
|
||||
@@ -202,7 +201,6 @@ impl fmt::Display for BlockError {
|
||||
RidiculousNumber(ref oob) => format!("Implausible block number. {}", oob),
|
||||
UnknownParent(ref hash) => format!("Unknown parent: {}", hash),
|
||||
UnknownUncleParent(ref hash) => format!("Unknown uncle parent: {}", hash),
|
||||
DoubleVote(ref address) => format!("Author {} issued too many blocks.", address),
|
||||
};
|
||||
|
||||
f.write_fmt(format_args!("Block error ({})", msg))
|
||||
@@ -263,6 +261,8 @@ pub enum Error {
|
||||
Snappy(::util::snappy::InvalidInput),
|
||||
/// Snapshot error.
|
||||
Snapshot(SnapshotError),
|
||||
/// Consensus vote error.
|
||||
Engine(EngineError),
|
||||
/// Ethkey error.
|
||||
Ethkey(EthkeyError),
|
||||
}
|
||||
@@ -285,6 +285,7 @@ impl fmt::Display for Error {
|
||||
Error::StdIo(ref err) => err.fmt(f),
|
||||
Error::Snappy(ref err) => err.fmt(f),
|
||||
Error::Snapshot(ref err) => err.fmt(f),
|
||||
Error::Engine(ref err) => err.fmt(f),
|
||||
Error::Ethkey(ref err) => err.fmt(f),
|
||||
}
|
||||
}
|
||||
@@ -383,6 +384,12 @@ impl From<SnapshotError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EngineError> for Error {
|
||||
fn from(err: EngineError) -> Error {
|
||||
Error::Engine(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EthkeyError> for Error {
|
||||
fn from(err: EthkeyError) -> Error {
|
||||
Error::Ethkey(err)
|
||||
|
||||
@@ -26,12 +26,12 @@ use state::{State, CleanupMode};
|
||||
use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockId, CallAnalytics, TransactionId};
|
||||
use client::TransactionImportResult;
|
||||
use executive::contract_address;
|
||||
use block::{ClosedBlock, SealedBlock, IsBlock, Block};
|
||||
use block::{ClosedBlock, IsBlock, Block};
|
||||
use error::*;
|
||||
use transaction::{Action, SignedTransaction, PendingTransaction};
|
||||
use receipt::{Receipt, RichReceipt};
|
||||
use spec::Spec;
|
||||
use engines::Engine;
|
||||
use engines::{Engine, Seal};
|
||||
use miner::{MinerService, MinerStatus, TransactionQueue, PrioritizationStrategy, AccountDetails, TransactionOrigin};
|
||||
use miner::banning_queue::{BanningTransactionQueue, Threshold};
|
||||
use miner::work_notify::WorkPoster;
|
||||
@@ -467,34 +467,43 @@ impl Miner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to perform internal sealing (one that does not require work) to return Ok(sealed),
|
||||
/// Err(Some(block)) returns for unsuccesful sealing while Err(None) indicates misspecified engine.
|
||||
fn seal_block_internally(&self, block: ClosedBlock) -> Result<SealedBlock, Option<ClosedBlock>> {
|
||||
trace!(target: "miner", "seal_block_internally: attempting internal seal.");
|
||||
let s = self.engine.generate_seal(block.block());
|
||||
if let Some(seal) = s {
|
||||
trace!(target: "miner", "seal_block_internally: managed internal seal. importing...");
|
||||
block.lock().try_seal(&*self.engine, seal).or_else(|(e, _)| {
|
||||
warn!("prepare_sealing: ERROR: try_seal failed when given internally generated seal: {}", e);
|
||||
Err(None)
|
||||
})
|
||||
} else {
|
||||
trace!(target: "miner", "seal_block_internally: unable to generate seal internally");
|
||||
Err(Some(block))
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses Engine to seal the block internally and then imports it to chain.
|
||||
/// Attempts to perform internal sealing (one that does not require work) and handles the result depending on the type of Seal.
|
||||
fn seal_and_import_block_internally(&self, chain: &MiningBlockChainClient, block: ClosedBlock) -> bool {
|
||||
if !block.transactions().is_empty() || self.forced_sealing() {
|
||||
if let Ok(sealed) = self.seal_block_internally(block) {
|
||||
if chain.import_sealed_block(sealed).is_ok() {
|
||||
trace!(target: "miner", "import_block_internally: imported internally sealed block");
|
||||
return true
|
||||
}
|
||||
trace!(target: "miner", "seal_block_internally: attempting internal seal.");
|
||||
match self.engine.generate_seal(block.block()) {
|
||||
// Save proposal for later seal submission and broadcast it.
|
||||
Seal::Proposal(seal) => {
|
||||
trace!(target: "miner", "Received a Proposal seal.");
|
||||
{
|
||||
let mut sealing_work = self.sealing_work.lock();
|
||||
sealing_work.queue.push(block.clone());
|
||||
sealing_work.queue.use_last_ref();
|
||||
}
|
||||
block
|
||||
.lock()
|
||||
.seal(&*self.engine, seal)
|
||||
.map(|sealed| { chain.broadcast_proposal_block(sealed); true })
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("ERROR: seal failed when given internally generated seal: {}", e);
|
||||
false
|
||||
})
|
||||
},
|
||||
// Directly import a regular sealed block.
|
||||
Seal::Regular(seal) =>
|
||||
block
|
||||
.lock()
|
||||
.seal(&*self.engine, seal)
|
||||
.map(|sealed| chain.import_sealed_block(sealed).is_ok())
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("ERROR: seal failed when given internally generated seal: {}", e);
|
||||
false
|
||||
}),
|
||||
Seal::None => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Prepares work which has to be done to seal.
|
||||
@@ -1035,7 +1044,6 @@ impl MinerService for Miner {
|
||||
self.transaction_queue.lock().last_nonce(address)
|
||||
}
|
||||
|
||||
|
||||
/// Update sealing if required.
|
||||
/// Prepare the block and work if the Engine does not seal internally.
|
||||
fn update_sealing(&self, chain: &MiningBlockChainClient) {
|
||||
@@ -1050,7 +1058,9 @@ impl MinerService for Miner {
|
||||
let (block, original_work_hash) = self.prepare_block(chain);
|
||||
if self.seals_internally {
|
||||
trace!(target: "miner", "update_sealing: engine indicates internal sealing");
|
||||
self.seal_and_import_block_internally(chain, block);
|
||||
if self.seal_and_import_block_internally(chain, block) {
|
||||
trace!(target: "miner", "update_sealing: imported internally sealed block");
|
||||
}
|
||||
} else {
|
||||
trace!(target: "miner", "update_sealing: engine does not seal internally, preparing work");
|
||||
self.prepare_work(block, original_work_hash);
|
||||
|
||||
@@ -20,7 +20,7 @@ use util::*;
|
||||
use io::*;
|
||||
use spec::Spec;
|
||||
use error::*;
|
||||
use client::{Client, ClientConfig, ChainNotify};
|
||||
use client::{Client, BlockChainClient, MiningBlockChainClient, ClientConfig, ChainNotify};
|
||||
use miner::Miner;
|
||||
use snapshot::ManifestData;
|
||||
use snapshot::service::{Service as SnapshotService, ServiceParams as SnapServiceParams};
|
||||
@@ -28,11 +28,9 @@ use std::sync::atomic::AtomicBool;
|
||||
|
||||
#[cfg(feature="ipc")]
|
||||
use nanoipc;
|
||||
#[cfg(feature="ipc")]
|
||||
use client::BlockChainClient;
|
||||
|
||||
/// Message type for external and internal events
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum ClientIoMessage {
|
||||
/// Best Block Hash in chain has been changed
|
||||
NewChainHead,
|
||||
@@ -50,6 +48,12 @@ pub enum ClientIoMessage {
|
||||
TakeSnapshot(u64),
|
||||
/// Trigger sealing update (useful for internal sealing).
|
||||
UpdateSealing,
|
||||
/// Submit seal (useful for internal sealing).
|
||||
SubmitSeal(H256, Vec<Bytes>),
|
||||
/// Broadcast a message to the network.
|
||||
BroadcastMessage(Bytes),
|
||||
/// New consensus message received.
|
||||
NewMessage(Bytes)
|
||||
}
|
||||
|
||||
/// Client service setup. Creates and registers client and network services with the IO subsystem.
|
||||
@@ -77,9 +81,6 @@ impl ClientService {
|
||||
panic_handler.forward_from(&io_service);
|
||||
|
||||
info!("Configured for {} using {} engine", Colour::White.bold().paint(spec.name.clone()), Colour::Yellow.bold().paint(spec.engine.name()));
|
||||
if spec.fork_name.is_some() {
|
||||
warn!("Your chain is an alternative fork. {}", Colour::Red.bold().paint("TRANSACTIONS MAY BE REPLAYED ON THE MAINNET!"));
|
||||
}
|
||||
|
||||
let mut db_config = DatabaseConfig::with_columns(::db::NUM_COLUMNS);
|
||||
|
||||
@@ -220,9 +221,11 @@ impl IoHandler<ClientIoMessage> for ClientIoHandler {
|
||||
debug!(target: "snapshot", "Failed to initialize periodic snapshot thread: {:?}", e);
|
||||
}
|
||||
},
|
||||
ClientIoMessage::UpdateSealing => {
|
||||
trace!(target: "authorityround", "message: UpdateSealing");
|
||||
self.client.update_sealing()
|
||||
ClientIoMessage::UpdateSealing => self.client.update_sealing(),
|
||||
ClientIoMessage::SubmitSeal(ref hash, ref seal) => self.client.submit_seal(*hash, seal.clone()),
|
||||
ClientIoMessage::BroadcastMessage(ref message) => self.client.broadcast_consensus_message(message.clone()),
|
||||
ClientIoMessage::NewMessage(ref message) => if let Err(e) = self.client.engine().handle_message(message) {
|
||||
trace!(target: "poa", "Invalid message received: {}", e);
|
||||
},
|
||||
_ => {} // ignore other messages
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ use service::ClientIoMessage;
|
||||
use views::HeaderView;
|
||||
|
||||
use io::IoChannel;
|
||||
use util::hash::H256;
|
||||
use util::{H256, Bytes};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -107,6 +107,7 @@ impl ChainNotify for Watcher {
|
||||
_: Vec<H256>,
|
||||
_: Vec<H256>,
|
||||
_: Vec<H256>,
|
||||
_: Vec<Bytes>,
|
||||
_duration: u64)
|
||||
{
|
||||
if self.oracle.is_major_importing() { return }
|
||||
@@ -174,6 +175,7 @@ mod tests {
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
//! Spec seal.
|
||||
|
||||
use rlp::*;
|
||||
use util::hash::{H64, H256};
|
||||
use util::hash::{H64, H256, H520};
|
||||
use ethjson;
|
||||
|
||||
/// Classic ethereum seal.
|
||||
@@ -32,23 +32,55 @@ impl Into<Generic> for Ethereum {
|
||||
fn into(self) -> Generic {
|
||||
let mut s = RlpStream::new_list(2);
|
||||
s.append(&self.mix_hash).append(&self.nonce);
|
||||
Generic {
|
||||
rlp: s.out()
|
||||
}
|
||||
Generic(s.out())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic seal.
|
||||
pub struct Generic {
|
||||
/// Seal rlp.
|
||||
pub rlp: Vec<u8>,
|
||||
/// AuthorityRound seal.
|
||||
pub struct AuthorityRound {
|
||||
/// Seal step.
|
||||
pub step: usize,
|
||||
/// Seal signature.
|
||||
pub signature: H520,
|
||||
}
|
||||
|
||||
/// Tendermint seal.
|
||||
pub struct Tendermint {
|
||||
/// Seal round.
|
||||
pub round: usize,
|
||||
/// Proposal seal signature.
|
||||
pub proposal: H520,
|
||||
/// Precommit seal signatures.
|
||||
pub precommits: Vec<H520>,
|
||||
}
|
||||
|
||||
impl Into<Generic> for AuthorityRound {
|
||||
fn into(self) -> Generic {
|
||||
let mut s = RlpStream::new_list(2);
|
||||
s.append(&self.step).append(&self.signature);
|
||||
Generic(s.out())
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Generic> for Tendermint {
|
||||
fn into(self) -> Generic {
|
||||
let mut s = RlpStream::new_list(3);
|
||||
s.append(&self.round).append(&self.proposal).append(&self.precommits);
|
||||
Generic(s.out())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Generic(pub Vec<u8>);
|
||||
|
||||
/// Genesis seal type.
|
||||
pub enum Seal {
|
||||
/// Classic ethereum seal.
|
||||
Ethereum(Ethereum),
|
||||
/// Generic seal.
|
||||
/// AuthorityRound seal.
|
||||
AuthorityRound(AuthorityRound),
|
||||
/// Tendermint seal.
|
||||
Tendermint(Tendermint),
|
||||
/// Generic RLP seal.
|
||||
Generic(Generic),
|
||||
}
|
||||
|
||||
@@ -59,9 +91,16 @@ impl From<ethjson::spec::Seal> for Seal {
|
||||
nonce: eth.nonce.into(),
|
||||
mix_hash: eth.mix_hash.into()
|
||||
}),
|
||||
ethjson::spec::Seal::Generic(g) => Seal::Generic(Generic {
|
||||
rlp: g.rlp.into()
|
||||
})
|
||||
ethjson::spec::Seal::AuthorityRound(ar) => Seal::AuthorityRound(AuthorityRound {
|
||||
step: ar.step.into(),
|
||||
signature: ar.signature.into()
|
||||
}),
|
||||
ethjson::spec::Seal::Tendermint(tender) => Seal::Tendermint(Tendermint {
|
||||
round: tender.round.into(),
|
||||
proposal: tender.proposal.into(),
|
||||
precommits: tender.precommits.into_iter().map(Into::into).collect()
|
||||
}),
|
||||
ethjson::spec::Seal::Generic(g) => Seal::Generic(Generic(g.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +109,9 @@ impl Into<Generic> for Seal {
|
||||
fn into(self) -> Generic {
|
||||
match self {
|
||||
Seal::Generic(generic) => generic,
|
||||
Seal::Ethereum(eth) => eth.into()
|
||||
Seal::Ethereum(eth) => eth.into(),
|
||||
Seal::AuthorityRound(ar) => ar.into(),
|
||||
Seal::Tendermint(tender) => tender.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
use util::*;
|
||||
use builtin::Builtin;
|
||||
use engines::{Engine, NullEngine, InstantSeal, BasicAuthority, AuthorityRound};
|
||||
use engines::{Engine, NullEngine, InstantSeal, BasicAuthority, AuthorityRound, Tendermint};
|
||||
use pod_state::*;
|
||||
use account_db::*;
|
||||
use header::{BlockNumber, Header};
|
||||
@@ -66,8 +66,8 @@ pub struct Spec {
|
||||
pub name: String,
|
||||
/// What engine are we using for this?
|
||||
pub engine: Arc<Engine>,
|
||||
/// The fork identifier for this chain. Only needed to distinguish two chains sharing the same genesis.
|
||||
pub fork_name: Option<String>,
|
||||
/// Name of the subdir inside the main data dir to use for chain data and settings.
|
||||
pub data_dir: String,
|
||||
|
||||
/// Known nodes on the network in enode format.
|
||||
pub nodes: Vec<String>,
|
||||
@@ -107,13 +107,13 @@ impl From<ethjson::spec::Spec> for Spec {
|
||||
fn from(s: ethjson::spec::Spec) -> Self {
|
||||
let builtins = s.accounts.builtins().into_iter().map(|p| (p.0.into(), From::from(p.1))).collect();
|
||||
let g = Genesis::from(s.genesis);
|
||||
let seal: GenericSeal = g.seal.into();
|
||||
let GenericSeal(seal_rlp) = g.seal.into();
|
||||
let params = CommonParams::from(s.params);
|
||||
Spec {
|
||||
name: s.name.into(),
|
||||
name: s.name.clone().into(),
|
||||
params: params.clone(),
|
||||
engine: Spec::engine(s.engine, params, builtins),
|
||||
fork_name: s.fork_name.map(Into::into),
|
||||
data_dir: s.data_dir.unwrap_or(s.name).into(),
|
||||
nodes: s.nodes.unwrap_or_else(Vec::new),
|
||||
parent_hash: g.parent_hash,
|
||||
transactions_root: g.transactions_root,
|
||||
@@ -124,7 +124,7 @@ impl From<ethjson::spec::Spec> for Spec {
|
||||
gas_used: g.gas_used,
|
||||
timestamp: g.timestamp,
|
||||
extra_data: g.extra_data,
|
||||
seal_rlp: seal.rlp,
|
||||
seal_rlp: seal_rlp,
|
||||
state_root_memo: RwLock::new(g.state_root),
|
||||
genesis_state: From::from(s.accounts),
|
||||
}
|
||||
@@ -146,7 +146,8 @@ impl Spec {
|
||||
ethjson::spec::Engine::InstantSeal => Arc::new(InstantSeal::new(params, builtins)),
|
||||
ethjson::spec::Engine::Ethash(ethash) => Arc::new(ethereum::Ethash::new(params, From::from(ethash.params), builtins)),
|
||||
ethjson::spec::Engine::BasicAuthority(basic_authority) => Arc::new(BasicAuthority::new(params, From::from(basic_authority.params), builtins)),
|
||||
ethjson::spec::Engine::AuthorityRound(authority_round) => AuthorityRound::new(params, From::from(authority_round.params), builtins).expect("Consensus engine could not be started."),
|
||||
ethjson::spec::Engine::AuthorityRound(authority_round) => AuthorityRound::new(params, From::from(authority_round.params), builtins).expect("Failed to start AuthorityRound consensus engine."),
|
||||
ethjson::spec::Engine::Tendermint(tendermint) => Tendermint::new(params, From::from(tendermint.params), builtins).expect("Failed to start the Tendermint consensus engine."),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +209,7 @@ impl Spec {
|
||||
|
||||
/// Overwrite the genesis components.
|
||||
pub fn overwrite_genesis_params(&mut self, g: Genesis) {
|
||||
let seal: GenericSeal = g.seal.into();
|
||||
let GenericSeal(seal_rlp) = g.seal.into();
|
||||
self.parent_hash = g.parent_hash;
|
||||
self.transactions_root = g.transactions_root;
|
||||
self.receipts_root = g.receipts_root;
|
||||
@@ -218,7 +219,7 @@ impl Spec {
|
||||
self.gas_used = g.gas_used;
|
||||
self.timestamp = g.timestamp;
|
||||
self.extra_data = g.extra_data;
|
||||
self.seal_rlp = seal.rlp;
|
||||
self.seal_rlp = seal_rlp;
|
||||
self.state_root_memo = RwLock::new(g.state_root);
|
||||
}
|
||||
|
||||
@@ -275,6 +276,10 @@ impl Spec {
|
||||
/// Create a new Spec with AuthorityRound consensus which does internal sealing (not requiring work).
|
||||
/// Accounts with secrets "0".sha3() and "1".sha3() are the authorities.
|
||||
pub fn new_test_round() -> Self { load_bundled!("authority_round") }
|
||||
|
||||
/// Create a new Spec with Tendermint consensus which does internal sealing (not requiring work).
|
||||
/// Account "0".sha3() and "1".sha3() are a authorities.
|
||||
pub fn new_test_tendermint() -> Self { load_bundled!("tendermint") }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -457,7 +457,6 @@ impl StateDB {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use util::{U256, H256, FixedHash, Address, DBTransaction};
|
||||
use tests::helpers::*;
|
||||
use state::Account;
|
||||
@@ -531,4 +530,3 @@ mod tests {
|
||||
assert!(s.get_cached_account(&address).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ use super::trace::{Action, Res};
|
||||
use header::BlockNumber;
|
||||
|
||||
/// Localized trace.
|
||||
#[derive(Debug, PartialEq, Binary)]
|
||||
#[derive(Debug, PartialEq, Clone, Binary)]
|
||||
pub struct LocalizedTrace {
|
||||
/// Type of action performed by a transaction.
|
||||
pub action: Action,
|
||||
|
||||
Reference in New Issue
Block a user