diff --git a/ethcore/client-traits/src/lib.rs b/ethcore/client-traits/src/lib.rs index 3968d3577..33d32c58f 100644 --- a/ethcore/client-traits/src/lib.rs +++ b/ethcore/client-traits/src/lib.rs @@ -444,9 +444,9 @@ impl TransactionRequest { self } - /// Sets a gas price. If this is not specified, a sensible default is used. - pub fn gas_price(mut self, gas_price: U256) -> TransactionRequest { - self.gas_price = Some(gas_price); + /// Sets a gas price. If this is not specified or `None`, a sensible default is used. + pub fn gas_price>>(mut self, gas_price: T) -> TransactionRequest { + self.gas_price = gas_price.into(); self } diff --git a/ethcore/engines/authority-round/src/lib.rs b/ethcore/engines/authority-round/src/lib.rs index adf057453..7d4c6e86c 100644 --- a/ethcore/engines/authority-round/src/lib.rs +++ b/ethcore/engines/authority-round/src/lib.rs @@ -60,6 +60,7 @@ use itertools::{self, Itertools}; use rand::rngs::OsRng; use rlp::{encode, Decodable, DecoderError, Encodable, RlpStream, Rlp}; use ethereum_types::{H256, H520, Address, U128, U256}; +use parity_bytes::Bytes; use parking_lot::{Mutex, RwLock}; use time_utils::CheckedSystemTime; use common_types::{ @@ -80,7 +81,7 @@ use common_types::{ transaction::SignedTransaction, }; use unexpected::{Mismatch, OutOfBounds}; -use validator_set::{ValidatorSet, SimpleList, new_validator_set}; +use validator_set::{ValidatorSet, SimpleList, new_validator_set_posdao}; mod finality; mod randomness; @@ -128,6 +129,9 @@ pub struct AuthorityRoundParams { /// The addresses of contracts that determine the block gas limit with their associated block /// numbers. pub block_gas_limit_contract_transitions: BTreeMap, + /// If set, this is the block number at which the consensus engine switches from AuRa to AuRa + /// with POSDAO modifications. + pub posdao_transition: Option, } const U16_MAX: usize = ::std::u16::MAX as usize; @@ -195,7 +199,7 @@ impl From for AuthorityRoundParams { .collect(); AuthorityRoundParams { step_durations, - validators: new_validator_set(p.validators), + validators: new_validator_set_posdao(p.validators, p.posdao_transition.map(Into::into)), start_step: p.start_step.map(Into::into), validate_score_transition: p.validate_score_transition.map_or(0, Into::into), validate_step_transition: p.validate_step_transition.map_or(0, Into::into), @@ -210,6 +214,7 @@ impl From for AuthorityRoundParams { strict_empty_steps_transition: p.strict_empty_steps_transition.map_or(0, Into::into), randomness_contract_address, block_gas_limit_contract_transitions, + posdao_transition: p.posdao_transition.map(Into::into), } } } @@ -598,6 +603,10 @@ pub struct AuthorityRound { block_gas_limit_contract_transitions: BTreeMap, /// Memoized gas limit overrides, by block hash. gas_limit_override_cache: Mutex>>, + /// The block number at which the consensus engine switches from AuRa to AuRa with POSDAO + /// modifications. For details about POSDAO, see the whitepaper: + /// https://www.xdaichain.com/for-validators/posdao-whitepaper + posdao_transition: Option, } // header-chain validator. @@ -893,6 +902,7 @@ impl AuthorityRound { randomness_contract_address: our_params.randomness_contract_address, block_gas_limit_contract_transitions: our_params.block_gas_limit_contract_transitions, gas_limit_override_cache: Mutex::new(LruCache::new(GAS_LIMIT_OVERRIDE_CACHE_CAPACITY)), + posdao_transition: our_params.posdao_transition, }); // Do not initialize timeouts for tests. @@ -1131,6 +1141,53 @@ impl AuthorityRound { EngineError::RequiresClient }) } + + fn run_posdao(&self, block: &ExecutedBlock, nonce: Option) -> Result, Error> { + // Skip the rest of the function unless there has been a transition to POSDAO AuRa. + if self.posdao_transition.map_or(true, |posdao_block| block.header.number() < posdao_block) { + trace!(target: "engine", "Skipping POSDAO calls to validator set contracts"); + return Ok(Vec::new()); + } + + let opt_signer = self.signer.read(); + let signer = match opt_signer.as_ref() { + Some(signer) => signer, + None => return Ok(Vec::new()), // We are not a validator, so we shouldn't call the contracts. + }; + let our_addr = signer.address(); + let client = self.upgrade_client_or("Unable to prepare block")?; + let full_client = client.as_full_client().ok_or_else(|| { + EngineError::FailedSystemCall("Failed to upgrade to BlockchainClient.".to_string()) + })?; + + // Makes a constant contract call. + let mut call = |to: Address, data: Bytes| { + full_client.call_contract(BlockId::Latest, to, data).map_err(|e| format!("{}", e)) + }; + + // Our current account nonce. The transactions must have consecutive nonces, starting with this one. + let mut tx_nonce = if let Some(tx_nonce) = nonce { + tx_nonce + } else { + block.state.nonce(&our_addr)? + }; + let mut transactions = Vec::new(); + + // Creates and signs a transaction with the given contract call. + let mut make_transaction = |to: Address, data: Bytes| -> Result { + let tx_request = TransactionRequest::call(to, data).gas_price(U256::zero()).nonce(tx_nonce); + tx_nonce += U256::one(); // Increment the nonce for the next transaction. + Ok(full_client.create_transaction(tx_request)?) + }; + + // Genesis is never a new block, but might as well check. + let first = block.header.number() == 0; + for (addr, data) in self.validators.generate_engine_transactions(first, &block.header, &mut call)? { + transactions.push(make_transaction(addr, data)?); + } + + Ok(transactions) + } } fn unix_now() -> Duration { @@ -1519,7 +1576,10 @@ impl Engine for AuthorityRound { } fn generate_engine_transactions(&self, block: &ExecutedBlock) -> Result, Error> { - self.run_randomness_phase(block) + let mut transactions = self.run_randomness_phase(block)?; + let nonce = transactions.last().map(|tx| tx.nonce + U256::one()); + transactions.extend(self.run_posdao(block, nonce)?); + Ok(transactions) } /// Check the number of seal fields. @@ -1976,6 +2036,7 @@ mod tests { two_thirds_majority_transition: 0, randomness_contract_address: BTreeMap::new(), block_gas_limit_contract_transitions: BTreeMap::new(), + posdao_transition: Some(0), }; // mutate aura params diff --git a/ethcore/engines/validator-set/res/validator_report.json b/ethcore/engines/validator-set/res/validator_report.json index 093f4bebd..e0c011432 100644 --- a/ethcore/engines/validator-set/res/validator_report.json +++ b/ethcore/engines/validator-set/res/validator_report.json @@ -1,4 +1,5 @@ [ {"constant":false,"inputs":[{"name":"validator","type":"address"},{"name":"blockNumber","type":"uint256"},{"name":"proof","type":"bytes"}],"name":"reportMalicious","outputs":[],"payable":false,"type":"function"}, - {"constant":false,"inputs":[{"name":"validator","type":"address"},{"name":"blockNumber","type":"uint256"}],"name":"reportBenign","outputs":[],"payable":false,"type":"function"} + {"constant":false,"inputs":[{"name":"validator","type":"address"},{"name":"blockNumber","type":"uint256"}],"name":"reportBenign","outputs":[],"payable":false,"type":"function"}, + {"constant": true, "inputs": [ { "name": "validator", "type": "address" }, { "name": "blockNum", "type": "uint256" } ], "name": "maliceReportedForBlock", "outputs": [ { "name": "", "type": "address[]" } ], "payable": false, "stateMutability": "view", "type": "function" } ] diff --git a/ethcore/engines/validator-set/res/validator_set.json b/ethcore/engines/validator-set/res/validator_set.json index d861e16fd..3f51291bf 100644 --- a/ethcore/engines/validator-set/res/validator_set.json +++ b/ethcore/engines/validator-set/res/validator_set.json @@ -1,5 +1,55 @@ [ {"constant":false,"inputs":[],"name":"finalizeChange","outputs":[],"payable":false,"type":"function"}, {"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"validators","type":"address[]"}],"payable":false,"type":"function"}, - {"anonymous":false,"inputs":[{"indexed":true,"name":"_parent_hash","type":"bytes32"},{"indexed":false,"name":"_new_set","type":"address[]"}],"name":"InitiateChange","type":"event"} + {"anonymous":false,"inputs":[{"indexed":true,"name":"_parent_hash","type":"bytes32"},{"indexed":false,"name":"_new_set","type":"address[]"}],"name":"InitiateChange","type":"event"}, + { + "constant": true, + "inputs": [], + "name": "emitInitiateChangeCallable", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "emitInitiateChange", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_reportingValidator", + "type": "address" + }, + { + "name": "_maliciousValidator", + "type": "address" + }, + { + "name": "_blockNumber", + "type": "uint256" + } + ], + "name": "shouldValidatorReport", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } ] diff --git a/ethcore/engines/validator-set/src/contract.rs b/ethcore/engines/validator-set/src/contract.rs index b64a0e861..6821889f6 100644 --- a/ethcore/engines/validator-set/src/contract.rs +++ b/ethcore/engines/validator-set/src/contract.rs @@ -21,8 +21,8 @@ use std::sync::Weak; use parity_bytes::Bytes; use ethabi_contract::use_contract; -use ethereum_types::{H256, Address}; -use log::{warn, trace}; +use ethereum_types::{H256, U256, Address}; +use log::{info, warn, trace}; use machine::Machine; use parking_lot::RwLock; use common_types::{ @@ -31,6 +31,7 @@ use common_types::{ header::Header, errors::EthcoreError, engines::machine::{Call, AuxiliaryData}, + transaction, }; use client_traits::{EngineClient, TransactionRequest}; @@ -48,31 +49,63 @@ pub struct ValidatorContract { contract_address: Address, validators: ValidatorSafeContract, client: RwLock>>, // TODO [keorn]: remove + posdao_transition: Option, } impl ValidatorContract { - pub fn new(contract_address: Address) -> Self { + pub fn new(contract_address: Address, posdao_transition: Option) -> Self { ValidatorContract { contract_address, - validators: ValidatorSafeContract::new(contract_address), + validators: ValidatorSafeContract::new(contract_address, posdao_transition), client: RwLock::new(None), + posdao_transition, } } } impl ValidatorContract { - fn transact(&self, data: Bytes) -> Result<(), String> { - let client = self.client.read().as_ref() - .and_then(Weak::upgrade) - .ok_or_else(|| "No client!")?; + fn transact(&self, data: Bytes, gas_price: Option, client: &dyn EngineClient) -> Result<(), String> { + let full_client = client.as_full_client().ok_or("No full client!")?; + let tx_request = TransactionRequest::call(self.contract_address, data).gas_price(gas_price); + match full_client.transact(tx_request) { + Ok(()) | Err(transaction::Error::AlreadyImported) => Ok(()), + Err(e) => Err(e.to_string())?, + } + } - match client.as_full_client() { - Some(c) => { - c.transact(TransactionRequest::call(self.contract_address, data)) - .map_err(|e| format!("Transaction import error: {}", e))?; - Ok(()) - }, - None => Err("No full client!".into()), + fn do_report_malicious(&self, address: &Address, block: BlockNumber, proof: Bytes) -> Result<(), EthcoreError> { + let client = self.client.read().as_ref().and_then(Weak::upgrade).ok_or("No client!")?; + let latest = client.block_header(BlockId::Latest).ok_or("No latest block!")?; + if !self.contains(&latest.parent_hash(), address) { + warn!(target: "engine", "Not reporting {} on block {}: Not a validator", address, block); + return Ok(()); + } + let data = validator_report::functions::report_malicious::encode_input(*address, block, proof); + self.validators.enqueue_report(*address, block, data.clone()); + let gas_price = self.report_gas_price(latest.number()); + self.transact(data, gas_price, &*client)?; + warn!(target: "engine", "Reported malicious validator {} at block {}", address, block); + Ok(()) + } + + fn do_report_benign(&self, address: &Address, block: BlockNumber) -> Result<(), EthcoreError> { + let client = self.client.read().as_ref().and_then(Weak::upgrade).ok_or("No client!")?; + let latest = client.block_header(BlockId::Latest).ok_or("No latest block!")?; + let data = validator_report::functions::report_benign::encode_input(*address, block); + let gas_price = self.report_gas_price(latest.number()); + self.transact(data, gas_price, &*client)?; + warn!(target: "engine", "Benign report for validator {} at block {}", address, block); + Ok(()) + } + + /// Returns the gas price for report transactions. + /// + /// After `posdaoTransition`, this is zero. Otherwise it is the default (`None`). + fn report_gas_price(&self, block: BlockNumber) -> Option { + if self.posdao_transition? <= block { + Some(0.into()) + } else { + None } } } @@ -82,6 +115,16 @@ impl ValidatorSet for ValidatorContract { self.validators.default_caller(id) } + fn generate_engine_transactions(&self, first: bool, header: &Header, call: &mut SystemCall) + -> Result, EthcoreError> + { + self.validators.generate_engine_transactions(first, header, call) + } + + fn on_close_block(&self, header: &Header, address: &Address) -> Result<(), EthcoreError> { + self.validators.on_close_block(header, address) + } + fn on_epoch_begin(&self, first: bool, header: &Header, call: &mut SystemCall) -> Result<(), EthcoreError> { self.validators.on_epoch_begin(first, header, call) } @@ -120,19 +163,15 @@ impl ValidatorSet for ValidatorContract { } fn report_malicious(&self, address: &Address, _set_block: BlockNumber, block: BlockNumber, proof: Bytes) { - let data = validator_report::functions::report_malicious::encode_input(*address, block, proof); - match self.transact(data) { - Ok(_) => warn!(target: "engine", "Reported malicious validator {}", address), - Err(s) => warn!(target: "engine", "Validator {} could not be reported {}", address, s), + if let Err(s) = self.do_report_malicious(address, block, proof) { + warn!(target: "engine", "Validator {} could not be reported ({}) on block {}", address, s, block); } } fn report_benign(&self, address: &Address, _set_block: BlockNumber, block: BlockNumber) { trace!(target: "engine", "validator set recording benign misbehaviour at block #{} by {:#x}", block, address); - let data = validator_report::functions::report_benign::encode_input(*address, block); - match self.transact(data) { - Ok(_) => warn!(target: "engine", "Reported benign validator misbehaviour {}", address), - Err(s) => warn!(target: "engine", "Validator {} could not be reported {}", address, s), + if let Err(s) = self.do_report_benign(address, block) { + warn!(target: "engine", "Validator {} could not be reported ({}) on block {}", address, s, block); } } @@ -150,6 +189,7 @@ mod tests { use call_contract::CallContract; use common_types::{header::Header, ids::BlockId}; use client_traits::{BlockChainClient, ChainInfo, BlockInfo, TransactionRequest}; + use ethabi::FunctionOutputDecoder; use ethcore::{ miner::{self, MinerService}, test_helpers::generate_dummy_client_with_spec, @@ -167,7 +207,8 @@ mod tests { #[test] fn fetches_validators() { let client = generate_dummy_client_with_spec(spec::new_validator_contract); - let vc = Arc::new(ValidatorContract::new("0000000000000000000000000000000000000005".parse::
().unwrap())); + let addr: Address = "0000000000000000000000000000000000000005".parse().unwrap(); + let vc = Arc::new(ValidatorContract::new(addr, None)); vc.register_client(Arc::downgrade(&client) as _); let last_hash = client.best_block_header().hash(); assert!(vc.contains(&last_hash, &"7d577a597b2742b498cb5cf0c26cdcd726d39e6e".parse::
().unwrap())); @@ -198,6 +239,8 @@ mod tests { assert!(client.engine().verify_block_external(&header).is_err()); client.engine().step(); assert_eq!(client.chain_info().best_block_number, 0); + // `reportBenign` when the designated proposer releases block from the future (bad clock). + assert!(client.engine().verify_block_basic(&header).is_err()); // Now create one that is more in future. That one should be rejected and validator should be reported. let mut header = Header::default(); @@ -211,7 +254,7 @@ mod tests { // Seal a block. client.engine().step(); assert_eq!(client.chain_info().best_block_number, 1); - // Check if the unresponsive validator is `disliked`. + // Check if the unresponsive validator is `disliked`. "d8f2e0bf" accesses the field `disliked`.. assert_eq!( client.call_contract(BlockId::Latest, validator_contract, "d8f2e0bf".from_hex().unwrap()).unwrap().to_hex(), "0000000000000000000000007d577a597b2742b498cb5cf0c26cdcd726d39e6e" @@ -223,6 +266,9 @@ mod tests { client.engine().step(); client.engine().step(); assert_eq!(client.chain_info().best_block_number, 2); + let (data, decoder) = super::validator_report::functions::malice_reported_for_block::call(v1, 1); + let reported_enc = client.call_contract(BlockId::Latest, validator_contract, data).expect("call failed"); + assert_ne!(Vec::
::new(), decoder.decode(&reported_enc).expect("decoding failed")); // Check if misbehaving validator was removed. client.transact(TransactionRequest::call(Default::default(), Default::default())).unwrap(); diff --git a/ethcore/engines/validator-set/src/lib.rs b/ethcore/engines/validator-set/src/lib.rs index 05a86bf06..ba45c78b4 100644 --- a/ethcore/engines/validator-set/src/lib.rs +++ b/ethcore/engines/validator-set/src/lib.rs @@ -49,18 +49,35 @@ use self::contract::ValidatorContract; use self::safe_contract::ValidatorSafeContract; use self::multi::Multi; -/// Creates a validator set from spec. -pub fn new_validator_set(spec: ValidatorSpec) -> Box { +/// Creates a validator set from the given spec and initializes a transition to POSDAO AuRa consensus. +pub fn new_validator_set_posdao( + spec: ValidatorSpec, + posdao_transition: Option +) -> Box { match spec { - ValidatorSpec::List(list) => Box::new(SimpleList::new(list.into_iter().map(Into::into).collect())), - ValidatorSpec::SafeContract(address) => Box::new(ValidatorSafeContract::new(address.into())), - ValidatorSpec::Contract(address) => Box::new(ValidatorContract::new(address.into())), - ValidatorSpec::Multi(sequence) => Box::new( - Multi::new(sequence.into_iter().map(|(block, set)| (block.into(), new_validator_set(set))).collect()) - ), + ValidatorSpec::List(list) => + Box::new(SimpleList::new(list.into_iter().map(Into::into).collect())), + ValidatorSpec::SafeContract(address) => + Box::new(ValidatorSafeContract::new(address.into(), posdao_transition)), + ValidatorSpec::Contract(address) => + Box::new(ValidatorContract::new(address.into(), posdao_transition)), + ValidatorSpec::Multi(sequence) => Box::new(Multi::new( + sequence + .into_iter() + .map(|(block, set)| ( + block.into(), + new_validator_set_posdao(set, posdao_transition) + )) + .collect() + )), } } +/// Creates a validator set from the given spec. +pub fn new_validator_set(spec: ValidatorSpec) -> Box { + new_validator_set_posdao(spec, None) +} + /// A validator set. pub trait ValidatorSet: Send + Sync + 'static { /// Get the default "Call" helper, for use in general operation. @@ -68,6 +85,17 @@ pub trait ValidatorSet: Send + Sync + 'static { // a strict dependency on state always being available. fn default_caller(&self, block_id: BlockId) -> Box; + /// Called for each new block this node is creating. If this block is + /// the first block of an epoch, this is called *after* `on_epoch_begin()`, + /// but with the same parameters. + /// + /// Returns a list of contract calls to be pushed onto the new block. + fn generate_engine_transactions(&self, _first: bool, _header: &Header, _call: &mut SystemCall) + -> Result, EthcoreError>; + + /// Called on the close of every block. + fn on_close_block(&self, _header: &Header, _address: &Address) -> Result<(), EthcoreError>; + /// Checks if a given address is a validator, /// using underlying, default call mechanism. fn contains(&self, parent: &H256, address: &Address) -> bool { diff --git a/ethcore/engines/validator-set/src/multi.rs b/ethcore/engines/validator-set/src/multi.rs index 76d7a4669..9a3c7778e 100644 --- a/ethcore/engines/validator-set/src/multi.rs +++ b/ethcore/engines/validator-set/src/multi.rs @@ -51,6 +51,14 @@ impl Multi { } } + fn map_children(&self, header: &Header, mut func: F) -> Result + where F: FnMut(&dyn ValidatorSet, bool) -> Result + { + let (set_block, set) = self.correct_set_by_number(header.number()); + let first = set_block == header.number(); + func(set, first) + } + fn correct_set(&self, id: BlockId) -> Option<&dyn ValidatorSet> { match self.block_number.read()(id).map(|parent_block| self.correct_set_by_number(parent_block)) { Ok((_, set)) => Some(set), @@ -82,11 +90,20 @@ impl ValidatorSet for Multi { .unwrap_or_else(|| Box::new(|_, _| Err("No validator set for given ID.".into()))) } - fn on_epoch_begin(&self, _first: bool, header: &Header, call: &mut SystemCall) -> Result<(), EthcoreError> { - let (set_block, set) = self.correct_set_by_number(header.number()); - let first = set_block == header.number(); + fn generate_engine_transactions(&self, _first: bool, header: &Header, call: &mut SystemCall) + -> Result, EthcoreError> + { + self.map_children(header, &mut |set: &dyn ValidatorSet, first| { + set.generate_engine_transactions(first, header, call) + }) + } - set.on_epoch_begin(first, header, call) + fn on_close_block(&self, header: &Header, address: &Address) -> Result<(), EthcoreError> { + self.map_children(header, &mut |set: &dyn ValidatorSet, _first| set.on_close_block(header, address)) + } + + fn on_epoch_begin(&self, _first: bool, header: &Header, call: &mut SystemCall) -> Result<(), EthcoreError> { + self.map_children(header, &mut |set: &dyn ValidatorSet, first| set.on_epoch_begin(first, header, call)) } fn genesis_epoch_data(&self, header: &Header, call: &Call) -> Result, String> { diff --git a/ethcore/engines/validator-set/src/safe_contract.rs b/ethcore/engines/validator-set/src/safe_contract.rs index de470e027..06bdd0035 100644 --- a/ethcore/engines/validator-set/src/safe_contract.rs +++ b/ethcore/engines/validator-set/src/safe_contract.rs @@ -16,9 +16,10 @@ /// Validator set maintained in a contract, updated using `getValidators` method. -use std::sync::{Weak, Arc}; +use std::collections::VecDeque; +use std::sync::{Arc, Weak}; -use client_traits::EngineClient; +use client_traits::{BlockChainClient, EngineClient, TransactionRequest}; use common_types::{ BlockNumber, header::Header, @@ -27,6 +28,7 @@ use common_types::{ log_entry::LogEntry, engines::machine::{Call, AuxiliaryData, AuxiliaryRequest}, receipt::Receipt, + transaction::{self, Action, Transaction}, }; use ethabi::FunctionOutputDecoder; use ethabi_contract::use_contract; @@ -34,11 +36,11 @@ use ethereum_types::{H256, U256, Address, Bloom}; use keccak_hash::keccak; use kvdb::DBValue; use lazy_static::lazy_static; -use log::{debug, info, trace}; +use log::{debug, info, trace, warn}; use machine::Machine; use memory_cache::MemoryLruCache; use parity_bytes::Bytes; -use parking_lot::RwLock; +use parking_lot::{Mutex, RwLock}; use rlp::{Rlp, RlpStream}; use unexpected::Mismatch; @@ -47,6 +49,13 @@ use super::simple_list::SimpleList; use_contract!(validator_set, "res/validator_set.json"); +/// The maximum number of reports to keep queued. +const MAX_QUEUED_REPORTS: usize = 10; +/// The maximum number of malice reports to include when creating a new block. +const MAX_REPORTS_PER_BLOCK: usize = 10; +/// Don't re-send malice reports every block. Skip this many before retrying. +const REPORTS_SKIP_BLOCKS: u64 = 1; + const MEMOIZE_CAPACITY: usize = 500; // TODO: ethabi should be able to generate this. @@ -86,6 +95,12 @@ pub struct ValidatorSafeContract { /// is the validator set valid for the blocks following that hash. validators: RwLock>, client: RwLock>>, // TODO [keorn]: remove + report_queue: Mutex, + /// The block number where we resent the queued reports last time. + resent_reports_in_block: Mutex, + /// If set, this is the block number at which the consensus engine switches from AuRa to AuRa + /// with POSDAO modifications. + posdao_transition: Option, } // first proof is just a state proof call of `getValidators` at header's state. @@ -103,8 +118,6 @@ fn encode_first_proof(header: &Header, state_items: &[Vec]) -> Bytes { fn check_first_proof(machine: &Machine, contract_address: Address, old_header: Header, state_items: &[DBValue]) -> Result, String> { - use common_types::transaction::{Action, Transaction}; - // TODO: match client contract_call_tx more cleanly without duplication. const PROVIDED_GAS: u64 = 50_000_000; @@ -200,14 +213,44 @@ fn prove_initial(contract_address: Address, header: &Header, caller: &Call) -> R } impl ValidatorSafeContract { - pub fn new(contract_address: Address) -> Self { + pub fn new(contract_address: Address, posdao_transition: Option) -> Self { ValidatorSafeContract { contract_address, validators: RwLock::new(MemoryLruCache::new(MEMOIZE_CAPACITY)), client: RwLock::new(None), + report_queue: Mutex::new(ReportQueue::default()), + resent_reports_in_block: Mutex::new(0), + posdao_transition, } } + fn transact(&self, data: Bytes, nonce: U256) -> Result<(), EthcoreError> { + let client = self.client.read().as_ref().and_then(Weak::upgrade).ok_or("No client!")?; + let full_client = client.as_full_client().ok_or("No full client!")?; + + let tx_request = TransactionRequest::call(self.contract_address, data).gas_price(U256::zero()).nonce(nonce); + match full_client.transact(tx_request) { + Ok(()) | Err(transaction::Error::AlreadyImported) => Ok(()), + Err(e) => Err(e)?, + } + } + + /// Puts a malice report into the queue for later resending. + /// + /// # Arguments + /// + /// * `addr` - The address of the misbehaving validator. + /// * `block` - The block number at which the misbehavior occurred. + /// * `data` - The call data for the `reportMalicious` contract call. + pub(crate) fn enqueue_report(&self, addr: Address, block: BlockNumber, data: Vec) { + // Skip the rest of the function unless there has been a transition to POSDAO AuRa. + if self.posdao_transition.map_or(true, |block_num| block < block_num) { + trace!(target: "engine", "Skipping queueing a malicious behavior report"); + return; + } + self.report_queue.lock().push(addr, block, data) + } + /// Queries the state and gets the set of validators. fn get_list(&self, caller: &Call) -> Option { let contract_address = self.contract_address; @@ -300,6 +343,85 @@ impl ValidatorSet for ValidatorSafeContract { .map(|out| (out, Vec::new()))) // generate no proofs in general } + fn generate_engine_transactions(&self, _first: bool, header: &Header, caller: &mut SystemCall) + -> Result, EthcoreError> + { + // Skip the rest of the function unless there has been a transition to POSDAO AuRa. + if self.posdao_transition.map_or(true, |block_num| header.number() < block_num) { + trace!(target: "engine", "Skipping a call to emitInitiateChange"); + return Ok(Vec::new()); + } + let mut transactions = Vec::new(); + + // Create the `InitiateChange` event if necessary. + let (data, decoder) = validator_set::functions::emit_initiate_change_callable::call(); + let emit_initiate_change_callable = caller(self.contract_address, data) + .and_then(|x| decoder.decode(&x) + .map_err(|x| format!("chain spec bug: could not decode: {:?}", x))) + .map_err(EngineError::FailedSystemCall)?; + if !emit_initiate_change_callable { + trace!(target: "engine", "New block #{} issued ― no need to call emitInitiateChange()", header.number()); + } else { + trace!(target: "engine", "New block issued #{} ― calling emitInitiateChange()", header.number()); + let (data, _decoder) = validator_set::functions::emit_initiate_change::call(); + transactions.push((self.contract_address, data)); + } + + let client = self.client.read().as_ref().and_then(Weak::upgrade).ok_or("No client!")?; + let client = client.as_full_client().ok_or("No full client!")?; + + // Retry all pending reports. + let mut report_queue = self.report_queue.lock(); + report_queue.filter(client, header.author(), self.contract_address); + for (_address, _block, data) in report_queue.iter().take(MAX_REPORTS_PER_BLOCK) { + transactions.push((self.contract_address, data.clone())) + } + + Ok(transactions) + } + + fn on_close_block(&self, header: &Header, our_address: &Address) -> Result<(), EthcoreError> { + // Skip the rest of the function unless there has been a transition to POSDAO AuRa. + if self.posdao_transition.map_or(true, |block_num| header.number() < block_num) { + trace!(target: "engine", "Skipping resending of queued malicious behavior reports"); + return Ok(()); + } + + let client = self.client.read().as_ref().and_then(Weak::upgrade).ok_or("No client!")?; + let client = client.as_full_client().ok_or("No full client!")?; + + let mut report_queue = self.report_queue.lock(); + report_queue.filter(client, our_address, self.contract_address); + report_queue.truncate(); + + let mut resent_reports_in_block = self.resent_reports_in_block.lock(); + + // Skip at least one block after sending malicious reports last time. + if header.number() > *resent_reports_in_block + REPORTS_SKIP_BLOCKS { + *resent_reports_in_block = header.number(); + let mut nonce = client.latest_nonce(our_address); + for (address, block, data) in report_queue.iter() { + debug!(target: "engine", "Retrying to report validator {} for misbehavior on block {} with nonce {}.", + address, block, nonce); + while match self.transact(data.clone(), nonce) { + Ok(()) => false, + Err(EthcoreError::Transaction(transaction::Error::Old)) => true, + Err(err) => { + warn!(target: "engine", "Cannot report validator {} for misbehavior on block {}: {}", + address, block, err); + false + } + } { + warn!(target: "engine", "Nonce {} already used. Incrementing.", nonce); + nonce += U256::from(1); + } + nonce += U256::from(1); + } + } + + Ok(()) + } + fn on_epoch_begin(&self, _first: bool, _header: &Header, caller: &mut SystemCall) -> Result<(), EthcoreError> { let data = validator_set::functions::finalize_change::encode_input(); caller(self.contract_address, data) @@ -450,6 +572,68 @@ impl ValidatorSet for ValidatorSafeContract { } } +/// A queue containing pending reports of malicious validators. +#[derive(Debug, Default)] +struct ReportQueue(VecDeque<(Address, BlockNumber, Vec)>); + +impl ReportQueue { + /// Pushes a report to the end of the queue. + fn push(&mut self, addr: Address, block: BlockNumber, data: Vec) { + self.0.push_back((addr, block, data)); + } + + /// Filters reports of validators that have already been reported or are banned. + fn filter( + &mut self, + client: &dyn BlockChainClient, + our_address: &Address, + contract_address: Address, + ) { + self.0.retain(|&(malicious_validator_address, block, ref _data)| { + trace!( + target: "engine", + "Checking if report of malicious validator {} at block {} should be removed from cache", + malicious_validator_address, + block + ); + // Check if the validator should be reported. + let (data, decoder) = validator_set::functions::should_validator_report::call( + *our_address, malicious_validator_address, block + ); + match client.call_contract(BlockId::Latest, contract_address, data) + .and_then(|result| decoder.decode(&result[..]).map_err(|e| e.to_string())) + { + Ok(false) => { + trace!(target: "engine", "Successfully removed report from report cache"); + false + } + Ok(true) => true, + Err(err) => { + warn!(target: "engine", "Failed to query report status {:?}, dropping pending report.", err); + false + } + } + }); + } + + /// Returns an iterator over all transactions in the queue. + fn iter(&self) -> impl Iterator)> { + self.0.iter() + } + + /// Removes reports from the queue if it contains more than `MAX_QUEUED_REPORTS` entries. + fn truncate(&mut self) { + if self.0.len() > MAX_QUEUED_REPORTS { + warn!( + target: "engine", + "Removing {} reports from report cache, even though it has not been finalized", + self.0.len() - MAX_QUEUED_REPORTS + ); + self.0.truncate(MAX_QUEUED_REPORTS); + } + } +} + #[cfg(test)] mod tests { use std::sync::Arc; @@ -481,7 +665,8 @@ mod tests { #[test] fn fetches_validators() { let client = generate_dummy_client_with_spec(spec::new_validator_safe_contract); - let vc = Arc::new(ValidatorSafeContract::new("0000000000000000000000000000000000000005".parse::
().unwrap())); + let addr: Address = "0000000000000000000000000000000000000005".parse().unwrap(); + let vc = Arc::new(ValidatorSafeContract::new(addr, None)); vc.register_client(Arc::downgrade(&client) as _); let last_hash = client.best_block_header().hash(); assert!(vc.contains(&last_hash, &"7d577a597b2742b498cb5cf0c26cdcd726d39e6e".parse::
().unwrap())); diff --git a/ethcore/engines/validator-set/src/simple_list.rs b/ethcore/engines/validator-set/src/simple_list.rs index cbcfd6cf1..def51ccb9 100644 --- a/ethcore/engines/validator-set/src/simple_list.rs +++ b/ethcore/engines/validator-set/src/simple_list.rs @@ -26,9 +26,10 @@ use common_types::{ use ethereum_types::{H256, Address}; use log::warn; use machine::Machine; +use parity_bytes::Bytes; use parity_util_mem::MallocSizeOf; -use super::ValidatorSet; +use super::{SystemCall, ValidatorSet}; /// Validator set containing a known set of addresses. #[derive(Clone, Debug, PartialEq, Eq, Default, MallocSizeOf)] @@ -71,6 +72,16 @@ impl ValidatorSet for SimpleList { Box::new(|_, _| Err("Simple list doesn't require calls.".into())) } + fn generate_engine_transactions(&self, _first: bool, _header: &Header, _call: &mut SystemCall) + -> Result, EthcoreError> + { + Ok(Vec::new()) + } + + fn on_close_block(&self, _header: &Header, _address: &Address) -> Result<(), EthcoreError> { + Ok(()) + } + fn is_epoch_end(&self, first: bool, _chain_head: &Header) -> Option> { match first { true => Some(Vec::new()), // allow transition to fixed list, and instantly diff --git a/ethcore/engines/validator-set/src/test.rs b/ethcore/engines/validator-set/src/test.rs index 9544f1eaa..e2658ac17 100644 --- a/ethcore/engines/validator-set/src/test.rs +++ b/ethcore/engines/validator-set/src/test.rs @@ -33,7 +33,7 @@ use ethereum_types::{H256, Address}; use machine::Machine; use parity_bytes::Bytes; -use super::{ValidatorSet, SimpleList}; +use super::{SystemCall, ValidatorSet, SimpleList}; /// Set used for testing with a single validator. #[derive(Clone, MallocSizeOf, Debug)] @@ -82,6 +82,16 @@ impl ValidatorSet for TestSet { Box::new(|_, _| Err("Test set doesn't require calls.".into())) } + fn generate_engine_transactions(&self, _first: bool, _header: &Header, _call: &mut SystemCall) + -> Result, EthcoreError> + { + Ok(Vec::new()) + } + + fn on_close_block(&self, _header: &Header, _address: &Address) -> Result<(), EthcoreError> { + Ok(()) + } + fn is_epoch_end(&self, _first: bool, _chain_head: &Header) -> Option> { None } fn signals_epoch_end(&self, _: bool, _: &Header, _: AuxiliaryData) diff --git a/ethcore/res/validator_contract.json b/ethcore/res/validator_contract.json index 9d007ac23..841fd2249 100644 --- a/ethcore/res/validator_contract.json +++ b/ethcore/res/validator_contract.json @@ -37,7 +37,7 @@ "0000000000000000000000000000000000000004": { "balance": "1", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } }, "0000000000000000000000000000000000000005": { "balance": "1", - "constructor": "60a06040819052737d577a597b2742b498cb5cf0c26cdcd726d39e6e60609081527382a978b3f5962a5b0957d9ee9eef472ee55b42f1608052600080546002825581805290927f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5639182019291905b828111156100a25782518254600160a060020a031916600160a060020a0390911617825560209092019160019091019061006d565b5b506100cd9291505b808211156100c9578054600160a060020a03191681556001016100ab565b5090565b505034610000575b60005b60005481101561012f578060016000600084815481101561000057906000526020600020900160005b9054600160a060020a036101009290920a90041681526020810191909152604001600020555b6001016100d8565b5b505b610453806101416000396000f3006060604052361561005c5763ffffffff60e060020a60003504166335aa2e4481146100615780634d238c8e1461008d578063b7ab4db5146100a8578063c476dd4014610110578063d69f13bb14610172578063d8f2e0bf14610190575b610000565b34610000576100716004356101b9565b60408051600160a060020a039092168252519081900360200190f35b34610000576100a6600160a060020a03600435166101e9565b005b34610000576100b5610260565b60408051602080825283518183015283519192839290830191858101910280838382156100fd575b8051825260208311156100fd57601f1990920191602091820191016100dd565b5050509050019250505060405180910390f35b3461000057604080516020600460443581810135601f81018490048402850184019095528484526100a6948235600160a060020a03169460248035956064949293919092019181908401838280828437509496506102ca95505050505050565b005b34610000576100a6600160a060020a03600435166024356103eb565b005b3461000057610071610418565b60408051600160a060020a039092168252519081900360200190f35b600081815481101561000057906000526020600020900160005b915054906101000a9004600160a060020a031681565b6000805480600101828181548183558181151161022b5760008381526020902061022b9181019083015b808211156102275760008155600101610213565b5090565b5b505050916000526020600020900160005b8154600160a060020a038086166101009390930a92830292021916179055505b50565b60408051602081810183526000808352805484518184028101840190955280855292939290918301828280156102bf57602002820191906000526020600020905b8154600160a060020a031681526001909101906020018083116102a1575b505050505090505b90565b6000805460001981019081101561000057906000526020600020900160005b9054906101000a9004600160a060020a031660006001600086600160a060020a0316600160a060020a0316815260200190815260200160002054815481101561000057906000526020600020900160005b8154600160a060020a039384166101009290920a918202918402191617905583166000908152600160205260408120819055805460001981019081101561000057906000526020600020900160005b6101000a815490600160a060020a03021916905560008054809190600190038154818355818115116103e0576000838152602090206103e09181019083015b808211156102275760008155600101610213565b5090565b5b505050505b505050565b6002805473ffffffffffffffffffffffffffffffffffffffff1916600160a060020a0384161790555b5050565b600254600160a060020a0316815600a165627a7a72305820f7876e17abd5f0927fff16788b4b3c9028ed64e6db740d788b07fc5f0a8f10920029" + "constructor": "60c0604052737d577a597b2742b498cb5cf0c26cdcd726d39e6e60809081527382a978b3f5962a5b0957d9ee9eef472ee55b42f160a05261004390600290816100a3565b5034801561005057600080fd5b5060005b60025481101561009d5780600560006002848154811061007057fe5b6000918252602080832090910154600160a060020a03168352820192909252604001902055600101610054565b5061012f565b8280548282559060005260206000209081019282156100f8579160200282015b828111156100f85782518254600160a060020a031916600160a060020a039091161782556020909201916001909101906100c3565b50610104929150610108565b5090565b61012c91905b80821115610104578054600160a060020a031916815560010161010e565b90565b6108e28061013e6000396000f3fe608060405234801561001057600080fd5b50600436106100d1576000357c010000000000000000000000000000000000000000000000000000000090048063b56b366b1161008e578063b56b366b14610206578063b7ab4db514610282578063c476dd401461028a578063cbd2d5281461030f578063d69f13bb14610345578063d8f2e0bf14610371576100d1565b806335aa2e44146100d65780633d3b54581461010f578063752862111461012b5780639300c9261461013557806393b4e25e146101d8578063a92252ae146101e0575b600080fd5b6100f3600480360360208110156100ec57600080fd5b5035610379565b60408051600160a060020a039092168252519081900360200190f35b6101176103a0565b604080519115158252519081900360200190f35b6101336103a7565b005b6101336004803603602081101561014b57600080fd5b81019060208101813564010000000081111561016657600080fd5b82018360208201111561017857600080fd5b8035906020019184602083028401116401000000008311171561019a57600080fd5b9190808060200260200160405190810160405280939291908181526020018383602002808284376000920191909152509295506103a9945050505050565b6101336103c8565b610117600480360360208110156101f657600080fd5b5035600160a060020a031661044f565b6102326004803603604081101561021c57600080fd5b50600160a060020a038135169060200135610464565b60408051602080825283518183015283519192839290830191858101910280838360005b8381101561026e578181015183820152602001610256565b505050509050019250505060405180910390f35b6102326104e3565b610133600480360360608110156102a057600080fd5b600160a060020a03823516916020810135918101906060810160408201356401000000008111156102d057600080fd5b8201836020820111156102e257600080fd5b8035906020019184600183028401116401000000008311171561030457600080fd5b509092509050610545565b6101176004803603606081101561032557600080fd5b50600160a060020a038135811691602081013590911690604001356106eb565b6101336004803603604081101561035b57600080fd5b50600160a060020a038135169060200135610788565b6100f36107b8565b6002818154811061038657fe5b600091825260209091200154600160a060020a0316905081565b4315155b90565b565b80516103bc9060029060208401906107c7565b506103c56103c8565b50565b60014303407f55252fa6eee4741b4e24a74a70e9c11fd2c2281df8d6ea13126ff845f7825c8960026040518080602001828103825283818154815260200191508054801561043f57602002820191906000526020600020905b8154600160a060020a03168152600190910190602001808311610421575b50509250505060405180910390a2565b60016020526000908152604090205460ff1681565b600160a060020a03821660009081526003602090815260408083208484528252918290208054835181840281018401909452808452606093928301828280156104d657602002820191906000526020600020905b8154600160a060020a031681526001909101906020018083116104b8575b5050505050905092915050565b6060600280548060200260200160405190810160405280929190818152602001828054801561053b57602002820191906000526020600020905b8154600160a060020a0316815260019091019060200180831161051d575b5050505050905090565b600160a060020a0384166000818152600360209081526040808320878452825280832080546001818101835591855283852001805473ffffffffffffffffffffffffffffffffffffffff1916339081179091558585526004845282852089865284528285208186528452828520805460ff19908116841790915586865282855283862080549091169092179091556005909252909120546002805492939290919081106105ee57fe5b600091825260209091200154600160a060020a031614156106e45760028054600019810190811061061b57fe5b6000918252602080832090910154600160a060020a038881168452600590925260409092205460028054929093169291811061065357fe5b6000918252602080832091909101805473ffffffffffffffffffffffffffffffffffffffff1916600160a060020a039485161790559187168152600590915260408120556002805460001981019081106106a957fe5b6000918252602090912001805473ffffffffffffffffffffffffffffffffffffffff1916905560028054906106e2906000198301610839565b505b5050505050565b60004380831115610700576000915050610781565b60648111801561071257508260648203115b15610721576000915050610781565b600160a060020a03841660009081526001602052604090205460ff161561074c576000915050610781565b5050600160a060020a03808316600090815260046020908152604080832085845282528083209387168352929052205460ff16155b9392505050565b506000805473ffffffffffffffffffffffffffffffffffffffff1916600160a060020a0392909216919091179055565b600054600160a060020a031681565b828054828255906000526020600020908101928215610829579160200282015b82811115610829578251825473ffffffffffffffffffffffffffffffffffffffff1916600160a060020a039091161782556020909201916001909101906107e7565b50610835929150610862565b5090565b81548183558181111561085d5760008381526020902061085d918101908301610893565b505050565b6103a491905b8082111561083557805473ffffffffffffffffffffffffffffffffffffffff19168155600101610868565b6103a491905b80821115610835576000815560010161089956fea265627a7a7231582073e77a97d79ab4382d5a28644654c335489310e72aa6c8f737e424bdb6c2bbd664736f6c63430005100032" }, "0x7d577a597b2742b498cb5cf0c26cdcd726d39e6e": { "balance": "1606938044258990275541962092341162602522202993782792835301376" }, "0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1": { "balance": "1606938044258990275541962092341162602522202993782792835301376" } diff --git a/ethcore/res/validator_contract.sol b/ethcore/res/validator_contract.sol new file mode 100644 index 000000000..ccbfc52bf --- /dev/null +++ b/ethcore/res/validator_contract.sol @@ -0,0 +1,119 @@ +// Source for the test AuRa validator set contract. DO NOT USE IN PRODUCTION. +// +// Contains POSDAO features. The full POSDAO ValidatorSet contract production code is available at +// https://github.com/poanetwork/posdao-contracts/blob/master/contracts/ValidatorSetAuRa.sol +// +// The bytecode of this contract is included in `validator_contract.json` as the +// constructor of address `0x0000..0005`. + +pragma solidity ^0.5.0; + +contract TestValidatorSet { + + address public disliked; // contains the address of validator reported by `reportBenign` + mapping(address => bool) public isValidatorBanned; // if the validator is banned by `reportMalicious` + + // The initial set of validators + address[] public validators = [ + 0x7d577a597B2742b498Cb5Cf0C26cDCD726d39E6e, + 0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1 + ]; + + // The mappings used by POSDAO features testing (see `reportMalicious` and `shouldValidatorReport` functions below) + mapping(address => mapping(uint256 => address[])) private _maliceReportedForBlock; + mapping(address => mapping(uint256 => mapping(address => bool))) private _maliceReportedForBlockMapped; + mapping(address => uint256) private _validatorIndex; + + // The standard event to notify the engine about the validator set changing in the contract + event InitiateChange(bytes32 indexed parentHash, address[] newSet); + + constructor() public { + // Initialize validator indices to be able to correctly remove + // a malicious validator from the validator set later + for (uint i = 0; i < validators.length; i++) { + _validatorIndex[validators[i]] = i; + } + } + + // Emits an `InitiateChange` event with the current (or new) validator set + function emitInitiateChange() public { + emit InitiateChange(blockhash(block.number - 1), validators); + } + + // Applies a validator set change in production code. Does nothing in the test + function finalizeChange() pure public {} + + // Benign validator behaviour report. Kept here for regression testing + function reportBenign(address _validator, uint256) public { + disliked = _validator; + } + + // Removes a malicious validator from the list + function reportMalicious(address _validator, uint256 _blockNum, bytes calldata) external { + address reportingValidator = msg.sender; + + // Mark the `_validator` as reported by `reportingValidator` for the block `_blockNum` + _maliceReportedForBlock[_validator][_blockNum].push(reportingValidator); + _maliceReportedForBlockMapped[_validator][_blockNum][reportingValidator] = true; + isValidatorBanned[_validator] = true; + + // If the passed validator is in the validator set + if (validators[_validatorIndex[_validator]] == _validator) { + // Remove the validator from the set + validators[_validatorIndex[_validator]] = validators[validators.length - 1]; + delete _validatorIndex[_validator]; + delete validators[validators.length - 1]; + validators.length--; + } + } + + // Tests validator set changing and emitting the `InitiateChange` event + function setValidators(address[] memory _validators) public { + validators = _validators; + emitInitiateChange(); + } + + // Checks if `emitInitiateChange` can be called (used by POSDAO tests) + function emitInitiateChangeCallable() view public returns(bool) { + return block.number > 0; + } + + // Returns the current validator set + function getValidators() public view returns(address[] memory) { + return validators; + } + + // Returns the list of all validators that reported the given validator + // as malicious for the given block. Used by POSDAO tests + function maliceReportedForBlock(address _validator, uint256 _blockNum) public view returns(address[] memory) { + return _maliceReportedForBlock[_validator][_blockNum]; + } + + // Returns a boolean flag indicating whether the specified validator + // should report about some validator's misbehaviour at the specified block. + // Used by POSDAO tests. + // `_reportingValidator` is the address of validator who reports. + // `_maliciousValidator` is the address of malicious validator. + // `_blockNumber` is the block number at which the malicious validator misbehaved. + function shouldValidatorReport( + address _reportingValidator, + address _maliciousValidator, + uint256 _blockNumber + ) public view returns(bool) { + uint256 currentBlock = block.number; + if (_blockNumber > currentBlock) { + return false; + } + if (currentBlock > 100 && currentBlock - 100 > _blockNumber) { + return false; + } + if (isValidatorBanned[_maliciousValidator]) { + // We shouldn't report the malicious validator + // as it has already been reported and banned + return false; + } + // Return `false` if already reported by the same `_reportingValidator` for the same `_blockNumber` + return !_maliceReportedForBlockMapped[_maliciousValidator][_blockNumber][_reportingValidator]; + } + +} diff --git a/json/src/spec/authority_round.rs b/json/src/spec/authority_round.rs index b4d3166da..57e5fe5fa 100644 --- a/json/src/spec/authority_round.rs +++ b/json/src/spec/authority_round.rs @@ -100,6 +100,9 @@ pub struct AuthorityRoundParams { /// The addresses of contracts that determine the block gas limit starting from the block number /// associated with each of those contracts. pub block_gas_limit_contract_transitions: Option>, + /// The block number at which the consensus engine switches from AuRa to AuRa with POSDAO + /// modifications. + pub posdao_transition: Option, } /// Authority engine deserialization.