* make validator set aware of various states * fix updater build * clean up contract call * failing sync test * adjust tests * nicer indent [ci skip] * revert bound divisor
This commit is contained in:
parent
1164193019
commit
c1da49bbc4
@ -33,7 +33,7 @@
|
|||||||
"timestamp": "0x00",
|
"timestamp": "0x00",
|
||||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
"extraData": "0x",
|
"extraData": "0x",
|
||||||
"gasLimit": "0x2fefd8"
|
"gasLimit": "0x222222"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"accounts": {
|
||||||
"0000000000000000000000000000000000000001": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
|
"0000000000000000000000000000000000000001": { "balance": "1", "nonce": "1048576", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
"timestamp": "0x00",
|
"timestamp": "0x00",
|
||||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
"extraData": "0x",
|
"extraData": "0x",
|
||||||
"gasLimit": "0x2fefd8"
|
"gasLimit": "0x222222"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"accounts": {
|
||||||
"0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
|
"0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
"timestamp": "0x00",
|
"timestamp": "0x00",
|
||||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
"extraData": "0x",
|
"extraData": "0x",
|
||||||
"gasLimit": "0x2fefd8"
|
"gasLimit": "0x222222"
|
||||||
},
|
},
|
||||||
"accounts": {
|
"accounts": {
|
||||||
"0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
|
"0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } },
|
||||||
|
@ -253,7 +253,7 @@ impl Client {
|
|||||||
if let Some(reg_addr) = client.additional_params().get("registrar").and_then(|s| Address::from_str(s).ok()) {
|
if let Some(reg_addr) = client.additional_params().get("registrar").and_then(|s| Address::from_str(s).ok()) {
|
||||||
trace!(target: "client", "Found registrar at {}", reg_addr);
|
trace!(target: "client", "Found registrar at {}", reg_addr);
|
||||||
let weak = Arc::downgrade(&client);
|
let weak = Arc::downgrade(&client);
|
||||||
let registrar = Registry::new(reg_addr, move |a, d| weak.upgrade().ok_or("No client!".into()).and_then(|c| c.call_contract(a, d)));
|
let registrar = Registry::new(reg_addr, move |a, d| weak.upgrade().ok_or("No client!".into()).and_then(|c| c.call_contract(BlockId::Latest, a, d)));
|
||||||
*client.registrar.lock() = Some(registrar);
|
*client.registrar.lock() = Some(registrar);
|
||||||
}
|
}
|
||||||
Ok(client)
|
Ok(client)
|
||||||
@ -1445,7 +1445,7 @@ impl BlockChainClient for Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn call_contract(&self, address: Address, data: Bytes) -> Result<Bytes, String> {
|
fn call_contract(&self, block_id: BlockId, address: Address, data: Bytes) -> Result<Bytes, String> {
|
||||||
let from = Address::default();
|
let from = Address::default();
|
||||||
let transaction = Transaction {
|
let transaction = Transaction {
|
||||||
nonce: self.latest_nonce(&from),
|
nonce: self.latest_nonce(&from),
|
||||||
@ -1456,7 +1456,7 @@ impl BlockChainClient for Client {
|
|||||||
data: data,
|
data: data,
|
||||||
}.fake_sign(from);
|
}.fake_sign(from);
|
||||||
|
|
||||||
self.call(&transaction, BlockId::Latest, Default::default())
|
self.call(&transaction, block_id, Default::default())
|
||||||
.map_err(|e| format!("{:?}", e))
|
.map_err(|e| format!("{:?}", e))
|
||||||
.map(|executed| {
|
.map(|executed| {
|
||||||
executed.output
|
executed.output
|
||||||
|
@ -731,7 +731,7 @@ impl BlockChainClient for TestBlockChainClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn call_contract(&self, _address: Address, _data: Bytes) -> Result<Bytes, String> { Ok(vec![]) }
|
fn call_contract(&self, _id: BlockId, _address: Address, _data: Bytes) -> Result<Bytes, String> { Ok(vec![]) }
|
||||||
|
|
||||||
fn transact_contract(&self, address: Address, data: Bytes) -> Result<TransactionImportResult, EthcoreError> {
|
fn transact_contract(&self, address: Address, data: Bytes) -> Result<TransactionImportResult, EthcoreError> {
|
||||||
let transaction = Transaction {
|
let transaction = Transaction {
|
||||||
|
@ -254,7 +254,7 @@ pub trait BlockChainClient : Sync + Send {
|
|||||||
fn pruning_info(&self) -> PruningInfo;
|
fn pruning_info(&self) -> PruningInfo;
|
||||||
|
|
||||||
/// Like `call`, but with various defaults. Designed to be used for calling contracts.
|
/// Like `call`, but with various defaults. Designed to be used for calling contracts.
|
||||||
fn call_contract(&self, address: Address, data: Bytes) -> Result<Bytes, String>;
|
fn call_contract(&self, id: BlockId, address: Address, data: Bytes) -> Result<Bytes, String>;
|
||||||
|
|
||||||
/// Import a transaction: used for misbehaviour reporting.
|
/// Import a transaction: used for misbehaviour reporting.
|
||||||
fn transact_contract(&self, address: Address, data: Bytes) -> Result<TransactionImportResult, EthcoreError>;
|
fn transact_contract(&self, address: Address, data: Bytes) -> Result<TransactionImportResult, EthcoreError>;
|
||||||
|
@ -150,12 +150,12 @@ impl AuthorityRound {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step_proposer(&self, step: usize) -> Address {
|
fn step_proposer(&self, bh: &H256, step: usize) -> Address {
|
||||||
self.validators.get(step)
|
self.validators.get(bh, step)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_step_proposer(&self, step: usize, address: &Address) -> bool {
|
fn is_step_proposer(&self, bh: &H256, step: usize, address: &Address) -> bool {
|
||||||
self.step_proposer(step) == *address
|
self.step_proposer(bh, step) == *address
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_future_step(&self, step: usize) -> bool {
|
fn is_future_step(&self, step: usize) -> bool {
|
||||||
@ -249,7 +249,7 @@ impl Engine for AuthorityRound {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn seals_internally(&self) -> Option<bool> {
|
fn seals_internally(&self) -> Option<bool> {
|
||||||
Some(self.validators.contains(&self.signer.address()))
|
Some(self.signer.address() != Address::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to seal the block internally.
|
/// Attempt to seal the block internally.
|
||||||
@ -260,7 +260,7 @@ impl Engine for AuthorityRound {
|
|||||||
if self.proposed.load(AtomicOrdering::SeqCst) { return Seal::None; }
|
if self.proposed.load(AtomicOrdering::SeqCst) { return Seal::None; }
|
||||||
let header = block.header();
|
let header = block.header();
|
||||||
let step = self.step.load(AtomicOrdering::SeqCst);
|
let step = self.step.load(AtomicOrdering::SeqCst);
|
||||||
if self.is_step_proposer(step, header.author()) {
|
if self.is_step_proposer(header.parent_hash(), step, header.author()) {
|
||||||
if let Ok(signature) = self.signer.sign(header.bare_hash()) {
|
if let Ok(signature) = self.signer.sign(header.bare_hash()) {
|
||||||
trace!(target: "engine", "generate_seal: Issuing a block for step {}.", step);
|
trace!(target: "engine", "generate_seal: Issuing a block for step {}.", step);
|
||||||
self.proposed.store(true, AtomicOrdering::SeqCst);
|
self.proposed.store(true, AtomicOrdering::SeqCst);
|
||||||
@ -299,32 +299,33 @@ impl Engine for AuthorityRound {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the signature belongs to the correct proposer.
|
fn verify_block_unordered(&self, _header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
||||||
fn verify_block_unordered(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
Ok(())
|
||||||
let header_step = header_step(header)?;
|
}
|
||||||
|
|
||||||
|
/// Do the validator and gas limit validation.
|
||||||
|
fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
||||||
|
let step = header_step(header)?;
|
||||||
// Give one step slack if step is lagging, double vote is still not possible.
|
// Give one step slack if step is lagging, double vote is still not possible.
|
||||||
if self.is_future_step(header_step) {
|
if self.is_future_step(step) {
|
||||||
trace!(target: "engine", "verify_block_unordered: block from the future");
|
trace!(target: "engine", "verify_block_unordered: block from the future");
|
||||||
self.validators.report_benign(header.author());
|
self.validators.report_benign(header.author());
|
||||||
Err(BlockError::InvalidSeal)?
|
Err(BlockError::InvalidSeal)?
|
||||||
} else {
|
} else {
|
||||||
|
// Check if the signature belongs to a validator, can depend on parent state.
|
||||||
let proposer_signature = header_signature(header)?;
|
let proposer_signature = header_signature(header)?;
|
||||||
let correct_proposer = self.step_proposer(header_step);
|
let correct_proposer = self.step_proposer(header.parent_hash(), step);
|
||||||
if verify_address(&correct_proposer, &proposer_signature, &header.bare_hash())? {
|
if !verify_address(&correct_proposer, &proposer_signature, &header.bare_hash())? {
|
||||||
Ok(())
|
trace!(target: "engine", "verify_block_unordered: bad proposer for step: {}", step);
|
||||||
} else {
|
|
||||||
trace!(target: "engine", "verify_block_unordered: bad proposer for step: {}", header_step);
|
|
||||||
Err(EngineError::NotProposer(Mismatch { expected: correct_proposer, found: header.author().clone() }))?
|
Err(EngineError::NotProposer(Mismatch { expected: correct_proposer, found: header.author().clone() }))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
// Do not calculate difficulty for genesis blocks.
|
||||||
if header.number() == 0 {
|
if header.number() == 0 {
|
||||||
return Err(From::from(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() })));
|
return Err(From::from(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() })));
|
||||||
}
|
}
|
||||||
|
|
||||||
let step = header_step(header)?;
|
|
||||||
// Check if parent is from a previous step.
|
// Check if parent is from a previous step.
|
||||||
if step == header_step(parent)? {
|
if step == header_step(parent)? {
|
||||||
trace!(target: "engine", "Multiple blocks proposed for step {}.", step);
|
trace!(target: "engine", "Multiple blocks proposed for step {}.", step);
|
||||||
@ -412,7 +413,7 @@ mod tests {
|
|||||||
let mut header: Header = Header::default();
|
let mut header: Header = Header::default();
|
||||||
header.set_seal(vec![encode(&H520::default()).to_vec()]);
|
header.set_seal(vec![encode(&H520::default()).to_vec()]);
|
||||||
|
|
||||||
let verify_result = engine.verify_block_unordered(&header, None);
|
let verify_result = engine.verify_block_family(&header, &Default::default(), None);
|
||||||
assert!(verify_result.is_err());
|
assert!(verify_result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -450,10 +451,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn proposer_switching() {
|
fn proposer_switching() {
|
||||||
let mut header: Header = Header::default();
|
|
||||||
let tap = AccountProvider::transient_provider();
|
let tap = AccountProvider::transient_provider();
|
||||||
let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap();
|
let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap();
|
||||||
|
let mut parent_header: Header = Header::default();
|
||||||
|
parent_header.set_seal(vec![encode(&0usize).to_vec()]);
|
||||||
|
parent_header.set_gas_limit(U256::from_str("222222").unwrap());
|
||||||
|
let mut header: Header = Header::default();
|
||||||
|
header.set_number(1);
|
||||||
|
header.set_gas_limit(U256::from_str("222222").unwrap());
|
||||||
header.set_author(addr);
|
header.set_author(addr);
|
||||||
|
|
||||||
let engine = Spec::new_test_round().engine;
|
let engine = Spec::new_test_round().engine;
|
||||||
@ -462,17 +467,22 @@ mod tests {
|
|||||||
// Two validators.
|
// Two validators.
|
||||||
// Spec starts with step 2.
|
// Spec starts with step 2.
|
||||||
header.set_seal(vec![encode(&2usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
|
header.set_seal(vec![encode(&2usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
|
||||||
assert!(engine.verify_block_seal(&header).is_err());
|
assert!(engine.verify_block_family(&header, &parent_header, None).is_err());
|
||||||
header.set_seal(vec![encode(&1usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
|
header.set_seal(vec![encode(&1usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
|
||||||
assert!(engine.verify_block_seal(&header).is_ok());
|
assert!(engine.verify_block_family(&header, &parent_header, None).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_future_block() {
|
fn rejects_future_block() {
|
||||||
let mut header: Header = Header::default();
|
|
||||||
let tap = AccountProvider::transient_provider();
|
let tap = AccountProvider::transient_provider();
|
||||||
let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap();
|
let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap();
|
||||||
|
|
||||||
|
let mut parent_header: Header = Header::default();
|
||||||
|
parent_header.set_seal(vec![encode(&0usize).to_vec()]);
|
||||||
|
parent_header.set_gas_limit(U256::from_str("222222").unwrap());
|
||||||
|
let mut header: Header = Header::default();
|
||||||
|
header.set_number(1);
|
||||||
|
header.set_gas_limit(U256::from_str("222222").unwrap());
|
||||||
header.set_author(addr);
|
header.set_author(addr);
|
||||||
|
|
||||||
let engine = Spec::new_test_round().engine;
|
let engine = Spec::new_test_round().engine;
|
||||||
@ -481,8 +491,8 @@ mod tests {
|
|||||||
// Two validators.
|
// Two validators.
|
||||||
// Spec starts with step 2.
|
// Spec starts with step 2.
|
||||||
header.set_seal(vec![encode(&1usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
|
header.set_seal(vec![encode(&1usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
|
||||||
assert!(engine.verify_block_seal(&header).is_ok());
|
assert!(engine.verify_block_family(&header, &parent_header, None).is_ok());
|
||||||
header.set_seal(vec![encode(&5usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
|
header.set_seal(vec![encode(&5usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]);
|
||||||
assert!(engine.verify_block_seal(&header).is_err());
|
assert!(engine.verify_block_family(&header, &parent_header, None).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,14 +104,14 @@ impl Engine for BasicAuthority {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn seals_internally(&self) -> Option<bool> {
|
fn seals_internally(&self) -> Option<bool> {
|
||||||
Some(self.validators.contains(&self.signer.address()))
|
Some(self.signer.address() != Address::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to seal the block internally.
|
/// Attempt to seal the block internally.
|
||||||
fn generate_seal(&self, block: &ExecutedBlock) -> Seal {
|
fn generate_seal(&self, block: &ExecutedBlock) -> Seal {
|
||||||
let header = block.header();
|
let header = block.header();
|
||||||
let author = header.author();
|
let author = header.author();
|
||||||
if self.validators.contains(author) {
|
if self.validators.contains(header.parent_hash(), author) {
|
||||||
// account should be pernamently unlocked, otherwise sealing will fail
|
// account should be pernamently unlocked, otherwise sealing will fail
|
||||||
if let Ok(signature) = self.signer.sign(header.bare_hash()) {
|
if let Ok(signature) = self.signer.sign(header.bare_hash()) {
|
||||||
return Seal::Regular(vec![::rlp::encode(&(&H520::from(signature) as &[u8])).to_vec()]);
|
return Seal::Regular(vec![::rlp::encode(&(&H520::from(signature) as &[u8])).to_vec()]);
|
||||||
@ -133,20 +133,20 @@ impl Engine for BasicAuthority {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_block_unordered(&self, header: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> {
|
fn verify_block_unordered(&self, _header: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> {
|
||||||
use rlp::{UntrustedRlp, View};
|
|
||||||
|
|
||||||
// check the signature is legit.
|
|
||||||
let sig = UntrustedRlp::new(&header.seal()[0]).as_val::<H520>()?;
|
|
||||||
let signer = public_to_address(&recover(&sig.into(), &header.bare_hash())?);
|
|
||||||
if !self.validators.contains(&signer) {
|
|
||||||
return Err(BlockError::InvalidSeal)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> {
|
fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> result::Result<(), Error> {
|
||||||
// we should not calculate difficulty for genesis blocks
|
use rlp::{UntrustedRlp, View};
|
||||||
|
// Check if the signature belongs to a validator, can depend on parent state.
|
||||||
|
let sig = UntrustedRlp::new(&header.seal()[0]).as_val::<H520>()?;
|
||||||
|
let signer = public_to_address(&recover(&sig.into(), &header.bare_hash())?);
|
||||||
|
if !self.validators.contains(header.parent_hash(), &signer) {
|
||||||
|
return Err(BlockError::InvalidSeal)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not calculate difficulty for genesis blocks.
|
||||||
if header.number() == 0 {
|
if header.number() == 0 {
|
||||||
return Err(From::from(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() })));
|
return Err(From::from(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() })));
|
||||||
}
|
}
|
||||||
@ -239,7 +239,7 @@ mod tests {
|
|||||||
let mut header: Header = Header::default();
|
let mut header: Header = Header::default();
|
||||||
header.set_seal(vec![::rlp::encode(&H520::default()).to_vec()]);
|
header.set_seal(vec![::rlp::encode(&H520::default()).to_vec()]);
|
||||||
|
|
||||||
let verify_result = engine.verify_block_unordered(&header, None);
|
let verify_result = engine.verify_block_family(&header, &Default::default(), None);
|
||||||
assert!(verify_result.is_err());
|
assert!(verify_result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +95,8 @@ pub struct Tendermint {
|
|||||||
last_lock: AtomicUsize,
|
last_lock: AtomicUsize,
|
||||||
/// Bare hash of the proposed block, used for seal submission.
|
/// Bare hash of the proposed block, used for seal submission.
|
||||||
proposal: RwLock<Option<H256>>,
|
proposal: RwLock<Option<H256>>,
|
||||||
|
/// Hash of the proposal parent block.
|
||||||
|
proposal_parent: RwLock<H256>,
|
||||||
/// Set used to determine the current validators.
|
/// Set used to determine the current validators.
|
||||||
validators: Box<ValidatorSet + Send + Sync>,
|
validators: Box<ValidatorSet + Send + Sync>,
|
||||||
}
|
}
|
||||||
@ -114,11 +116,12 @@ impl Tendermint {
|
|||||||
height: AtomicUsize::new(1),
|
height: AtomicUsize::new(1),
|
||||||
view: AtomicUsize::new(0),
|
view: AtomicUsize::new(0),
|
||||||
step: RwLock::new(Step::Propose),
|
step: RwLock::new(Step::Propose),
|
||||||
votes: VoteCollector::default(),
|
votes: Default::default(),
|
||||||
signer: Default::default(),
|
signer: Default::default(),
|
||||||
lock_change: RwLock::new(None),
|
lock_change: RwLock::new(None),
|
||||||
last_lock: AtomicUsize::new(0),
|
last_lock: AtomicUsize::new(0),
|
||||||
proposal: RwLock::new(None),
|
proposal: RwLock::new(None),
|
||||||
|
proposal_parent: Default::default(),
|
||||||
validators: new_validator_set(our_params.validators),
|
validators: new_validator_set(our_params.validators),
|
||||||
});
|
});
|
||||||
let handler = TransitionHandler::new(Arc::downgrade(&engine) as Weak<Engine>, Box::new(our_params.timeouts));
|
let handler = TransitionHandler::new(Arc::downgrade(&engine) as Weak<Engine>, Box::new(our_params.timeouts));
|
||||||
@ -232,7 +235,7 @@ impl Tendermint {
|
|||||||
let height = self.height.load(AtomicOrdering::SeqCst);
|
let height = self.height.load(AtomicOrdering::SeqCst);
|
||||||
if let Some(block_hash) = *self.proposal.read() {
|
if let Some(block_hash) = *self.proposal.read() {
|
||||||
// Generate seal and remove old votes.
|
// Generate seal and remove old votes.
|
||||||
if self.is_signer_proposer() {
|
if self.is_signer_proposer(&*self.proposal_parent.read()) {
|
||||||
let proposal_step = VoteStep::new(height, view, Step::Propose);
|
let proposal_step = VoteStep::new(height, view, Step::Propose);
|
||||||
let precommit_step = VoteStep::new(proposal_step.height, proposal_step.view, Step::Precommit);
|
let precommit_step = VoteStep::new(proposal_step.height, proposal_step.view, Step::Precommit);
|
||||||
if let Some(seal) = self.votes.seal_signatures(proposal_step, precommit_step, &block_hash) {
|
if let Some(seal) = self.votes.seal_signatures(proposal_step, precommit_step, &block_hash) {
|
||||||
@ -254,23 +257,23 @@ impl Tendermint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_authority(&self, address: &Address) -> bool {
|
fn is_authority(&self, address: &Address) -> bool {
|
||||||
self.validators.contains(address)
|
self.validators.contains(&*self.proposal_parent.read(), address)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_above_threshold(&self, n: usize) -> bool {
|
fn is_above_threshold(&self, n: usize) -> bool {
|
||||||
n > self.validators.count() * 2/3
|
n > self.validators.count(&*self.proposal_parent.read()) * 2/3
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the designated for the given view.
|
/// Find the designated for the given view.
|
||||||
fn view_proposer(&self, height: Height, view: View) -> Address {
|
fn view_proposer(&self, bh: &H256, height: Height, view: View) -> Address {
|
||||||
let proposer_nonce = height + view;
|
let proposer_nonce = height + view;
|
||||||
trace!(target: "engine", "Proposer nonce: {}", proposer_nonce);
|
trace!(target: "engine", "Proposer nonce: {}", proposer_nonce);
|
||||||
self.validators.get(proposer_nonce)
|
self.validators.get(bh, proposer_nonce)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if address is a proposer for given view.
|
/// Check if address is a proposer for given view.
|
||||||
fn is_view_proposer(&self, height: Height, view: View, address: &Address) -> Result<(), EngineError> {
|
fn is_view_proposer(&self, bh: &H256, height: Height, view: View, address: &Address) -> Result<(), EngineError> {
|
||||||
let proposer = self.view_proposer(height, view);
|
let proposer = self.view_proposer(bh, height, view);
|
||||||
if proposer == *address {
|
if proposer == *address {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
@ -279,8 +282,8 @@ impl Tendermint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if current signer is the current proposer.
|
/// Check if current signer is the current proposer.
|
||||||
fn is_signer_proposer(&self) -> bool {
|
fn is_signer_proposer(&self, bh: &H256) -> bool {
|
||||||
let proposer = self.view_proposer(self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst));
|
let proposer = self.view_proposer(bh, self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst));
|
||||||
self.signer.is_address(&proposer)
|
self.signer.is_address(&proposer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,7 +422,7 @@ impl Engine for Tendermint {
|
|||||||
|
|
||||||
/// Should this node participate.
|
/// Should this node participate.
|
||||||
fn seals_internally(&self) -> Option<bool> {
|
fn seals_internally(&self) -> Option<bool> {
|
||||||
Some(self.is_authority(&self.signer.address()))
|
Some(self.signer.address() != Address::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to seal generate a proposal seal.
|
/// Attempt to seal generate a proposal seal.
|
||||||
@ -427,7 +430,7 @@ impl Engine for Tendermint {
|
|||||||
let header = block.header();
|
let header = block.header();
|
||||||
let author = header.author();
|
let author = header.author();
|
||||||
// Only proposer can generate seal if None was generated.
|
// Only proposer can generate seal if None was generated.
|
||||||
if !self.is_signer_proposer() || self.proposal.read().is_some() {
|
if !self.is_signer_proposer(header.parent_hash()) || self.proposal.read().is_some() {
|
||||||
return Seal::None;
|
return Seal::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,6 +444,7 @@ impl Engine for Tendermint {
|
|||||||
self.votes.vote(ConsensusMessage::new(signature, height, view, Step::Propose, bh), author);
|
self.votes.vote(ConsensusMessage::new(signature, height, view, Step::Propose, bh), author);
|
||||||
// Remember proposal for later seal submission.
|
// Remember proposal for later seal submission.
|
||||||
*self.proposal.write() = bh;
|
*self.proposal.write() = bh;
|
||||||
|
*self.proposal_parent.write() = header.parent_hash().clone();
|
||||||
Seal::Proposal(vec![
|
Seal::Proposal(vec![
|
||||||
::rlp::encode(&view).to_vec(),
|
::rlp::encode(&view).to_vec(),
|
||||||
::rlp::encode(&signature).to_vec(),
|
::rlp::encode(&signature).to_vec(),
|
||||||
@ -505,7 +509,12 @@ impl Engine for Tendermint {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_block_unordered(&self, header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
fn verify_block_unordered(&self, _header: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify validators and gas limit.
|
||||||
|
fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
||||||
let proposal = ConsensusMessage::new_proposal(header)?;
|
let proposal = ConsensusMessage::new_proposal(header)?;
|
||||||
let proposer = proposal.verify()?;
|
let proposer = proposal.verify()?;
|
||||||
if !self.is_authority(&proposer) {
|
if !self.is_authority(&proposer) {
|
||||||
@ -522,7 +531,7 @@ impl Engine for Tendermint {
|
|||||||
Some(a) => a,
|
Some(a) => a,
|
||||||
None => public_to_address(&recover(&precommit.signature.into(), &precommit_hash)?),
|
None => public_to_address(&recover(&precommit.signature.into(), &precommit_hash)?),
|
||||||
};
|
};
|
||||||
if !self.validators.contains(&address) {
|
if !self.validators.contains(header.parent_hash(), &address) {
|
||||||
Err(EngineError::NotAuthorized(address.to_owned()))?
|
Err(EngineError::NotAuthorized(address.to_owned()))?
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -545,12 +554,9 @@ impl Engine for Tendermint {
|
|||||||
found: signatures_len
|
found: signatures_len
|
||||||
}))?;
|
}))?;
|
||||||
}
|
}
|
||||||
self.is_view_proposer(proposal.vote_step.height, proposal.vote_step.view, &proposer)?;
|
self.is_view_proposer(header.parent_hash(), proposal.vote_step.height, proposal.vote_step.view, &proposer)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_block_family(&self, header: &Header, parent: &Header, _block: Option<&[u8]>) -> Result<(), Error> {
|
|
||||||
if header.number() == 0 {
|
if header.number() == 0 {
|
||||||
Err(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() }))?;
|
Err(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() }))?;
|
||||||
}
|
}
|
||||||
@ -595,6 +601,7 @@ impl Engine for Tendermint {
|
|||||||
debug!(target: "engine", "Received a new proposal {:?} from {}.", proposal.vote_step, proposer);
|
debug!(target: "engine", "Received a new proposal {:?} from {}.", proposal.vote_step, proposer);
|
||||||
if self.is_view(&proposal) {
|
if self.is_view(&proposal) {
|
||||||
*self.proposal.write() = proposal.block_hash.clone();
|
*self.proposal.write() = proposal.block_hash.clone();
|
||||||
|
*self.proposal_parent.write() = header.parent_hash().clone();
|
||||||
}
|
}
|
||||||
self.votes.vote(proposal, &proposer);
|
self.votes.vote(proposal, &proposer);
|
||||||
true
|
true
|
||||||
@ -607,7 +614,7 @@ impl Engine for Tendermint {
|
|||||||
trace!(target: "engine", "Propose timeout.");
|
trace!(target: "engine", "Propose timeout.");
|
||||||
if self.proposal.read().is_none() {
|
if self.proposal.read().is_none() {
|
||||||
// Report the proposer if no proposal was received.
|
// Report the proposer if no proposal was received.
|
||||||
let current_proposer = self.view_proposer(self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst));
|
let current_proposer = self.view_proposer(&*self.proposal_parent.read(), self.height.load(AtomicOrdering::SeqCst), self.view.load(AtomicOrdering::SeqCst));
|
||||||
self.validators.report_benign(¤t_proposer);
|
self.validators.report_benign(¤t_proposer);
|
||||||
}
|
}
|
||||||
Step::Prevote
|
Step::Prevote
|
||||||
@ -765,20 +772,25 @@ mod tests {
|
|||||||
let (spec, tap) = setup();
|
let (spec, tap) = setup();
|
||||||
let engine = spec.engine;
|
let engine = spec.engine;
|
||||||
|
|
||||||
let mut header = Header::default();
|
let mut parent_header: Header = Header::default();
|
||||||
let validator = insert_and_unlock(&tap, "0");
|
parent_header.set_gas_limit(U256::from_str("222222").unwrap());
|
||||||
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 mut header = Header::default();
|
||||||
|
header.set_number(1);
|
||||||
|
header.set_gas_limit(U256::from_str("222222").unwrap());
|
||||||
let validator = insert_and_unlock(&tap, "1");
|
let validator = insert_and_unlock(&tap, "1");
|
||||||
header.set_author(validator);
|
header.set_author(validator);
|
||||||
let seal = proposal_seal(&tap, &header, 0);
|
let seal = proposal_seal(&tap, &header, 0);
|
||||||
header.set_seal(seal);
|
header.set_seal(seal);
|
||||||
|
// Good proposer.
|
||||||
|
assert!(engine.verify_block_family(&header, &parent_header, None).is_ok());
|
||||||
|
|
||||||
|
let validator = insert_and_unlock(&tap, "0");
|
||||||
|
header.set_author(validator);
|
||||||
|
let seal = proposal_seal(&tap, &header, 0);
|
||||||
|
header.set_seal(seal);
|
||||||
// Bad proposer.
|
// Bad proposer.
|
||||||
match engine.verify_block_unordered(&header, None) {
|
match engine.verify_block_family(&header, &parent_header, None) {
|
||||||
Err(Error::Engine(EngineError::NotProposer(_))) => {},
|
Err(Error::Engine(EngineError::NotProposer(_))) => {},
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
}
|
}
|
||||||
@ -788,7 +800,7 @@ mod tests {
|
|||||||
let seal = proposal_seal(&tap, &header, 0);
|
let seal = proposal_seal(&tap, &header, 0);
|
||||||
header.set_seal(seal);
|
header.set_seal(seal);
|
||||||
// Not authority.
|
// Not authority.
|
||||||
match engine.verify_block_unordered(&header, None) {
|
match engine.verify_block_family(&header, &parent_header, None) {
|
||||||
Err(Error::Engine(EngineError::NotAuthorized(_))) => {},
|
Err(Error::Engine(EngineError::NotAuthorized(_))) => {},
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
};
|
};
|
||||||
@ -800,19 +812,24 @@ mod tests {
|
|||||||
let (spec, tap) = setup();
|
let (spec, tap) = setup();
|
||||||
let engine = spec.engine;
|
let engine = spec.engine;
|
||||||
|
|
||||||
|
let mut parent_header: Header = Header::default();
|
||||||
|
parent_header.set_gas_limit(U256::from_str("222222").unwrap());
|
||||||
|
|
||||||
let mut header = Header::default();
|
let mut header = Header::default();
|
||||||
|
header.set_number(2);
|
||||||
|
header.set_gas_limit(U256::from_str("222222").unwrap());
|
||||||
let proposer = insert_and_unlock(&tap, "1");
|
let proposer = insert_and_unlock(&tap, "1");
|
||||||
header.set_author(proposer);
|
header.set_author(proposer);
|
||||||
let mut seal = proposal_seal(&tap, &header, 0);
|
let mut seal = proposal_seal(&tap, &header, 0);
|
||||||
|
|
||||||
let vote_info = message_info_rlp(&VoteStep::new(0, 0, Step::Precommit), Some(header.bare_hash()));
|
let vote_info = message_info_rlp(&VoteStep::new(2, 0, Step::Precommit), Some(header.bare_hash()));
|
||||||
let signature1 = tap.sign(proposer, None, vote_info.sha3()).unwrap();
|
let signature1 = tap.sign(proposer, None, vote_info.sha3()).unwrap();
|
||||||
|
|
||||||
seal[2] = ::rlp::encode(&vec![H520::from(signature1.clone())]).to_vec();
|
seal[2] = ::rlp::encode(&vec![H520::from(signature1.clone())]).to_vec();
|
||||||
header.set_seal(seal.clone());
|
header.set_seal(seal.clone());
|
||||||
|
|
||||||
// One good signature is not enough.
|
// One good signature is not enough.
|
||||||
match engine.verify_block_unordered(&header, None) {
|
match engine.verify_block_family(&header, &parent_header, None) {
|
||||||
Err(Error::Engine(EngineError::BadSealFieldSize(_))) => {},
|
Err(Error::Engine(EngineError::BadSealFieldSize(_))) => {},
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
}
|
}
|
||||||
@ -823,7 +840,7 @@ mod tests {
|
|||||||
seal[2] = ::rlp::encode(&vec![H520::from(signature1.clone()), H520::from(signature0.clone())]).to_vec();
|
seal[2] = ::rlp::encode(&vec![H520::from(signature1.clone()), H520::from(signature0.clone())]).to_vec();
|
||||||
header.set_seal(seal.clone());
|
header.set_seal(seal.clone());
|
||||||
|
|
||||||
assert!(engine.verify_block_unordered(&header, None).is_ok());
|
assert!(engine.verify_block_family(&header, &parent_header, None).is_ok());
|
||||||
|
|
||||||
let bad_voter = insert_and_unlock(&tap, "101");
|
let bad_voter = insert_and_unlock(&tap, "101");
|
||||||
let bad_signature = tap.sign(bad_voter, None, vote_info.sha3()).unwrap();
|
let bad_signature = tap.sign(bad_voter, None, vote_info.sha3()).unwrap();
|
||||||
@ -832,7 +849,7 @@ mod tests {
|
|||||||
header.set_seal(seal);
|
header.set_seal(seal);
|
||||||
|
|
||||||
// One good and one bad signature.
|
// One good and one bad signature.
|
||||||
match engine.verify_block_unordered(&header, None) {
|
match engine.verify_block_family(&header, &parent_header, None) {
|
||||||
Err(Error::Engine(EngineError::NotAuthorized(_))) => {},
|
Err(Error::Engine(EngineError::NotAuthorized(_))) => {},
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
};
|
};
|
||||||
|
@ -26,30 +26,30 @@ use super::safe_contract::ValidatorSafeContract;
|
|||||||
/// The validator contract should have the following interface:
|
/// The validator contract should have the following interface:
|
||||||
/// [{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}]
|
/// [{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}]
|
||||||
pub struct ValidatorContract {
|
pub struct ValidatorContract {
|
||||||
validators: Arc<ValidatorSafeContract>,
|
validators: ValidatorSafeContract,
|
||||||
provider: RwLock<Option<provider::Contract>>,
|
provider: RwLock<Option<provider::Contract>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ValidatorContract {
|
impl ValidatorContract {
|
||||||
pub fn new(contract_address: Address) -> Self {
|
pub fn new(contract_address: Address) -> Self {
|
||||||
ValidatorContract {
|
ValidatorContract {
|
||||||
validators: Arc::new(ValidatorSafeContract::new(contract_address)),
|
validators: ValidatorSafeContract::new(contract_address),
|
||||||
provider: RwLock::new(None),
|
provider: RwLock::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ValidatorSet for Arc<ValidatorContract> {
|
impl ValidatorSet for ValidatorContract {
|
||||||
fn contains(&self, address: &Address) -> bool {
|
fn contains(&self, bh: &H256, address: &Address) -> bool {
|
||||||
self.validators.contains(address)
|
self.validators.contains(bh, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, nonce: usize) -> Address {
|
fn get(&self, bh: &H256, nonce: usize) -> Address {
|
||||||
self.validators.get(nonce)
|
self.validators.get(bh, nonce)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn count(&self) -> usize {
|
fn count(&self, bh: &H256) -> usize {
|
||||||
self.validators.count()
|
self.validators.count(bh)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn report_malicious(&self, address: &Address) {
|
fn report_malicious(&self, address: &Address) {
|
||||||
@ -144,6 +144,7 @@ mod tests {
|
|||||||
use header::Header;
|
use header::Header;
|
||||||
use account_provider::AccountProvider;
|
use account_provider::AccountProvider;
|
||||||
use miner::MinerService;
|
use miner::MinerService;
|
||||||
|
use types::ids::BlockId;
|
||||||
use client::BlockChainClient;
|
use client::BlockChainClient;
|
||||||
use tests::helpers::generate_dummy_client_with_spec_and_accounts;
|
use tests::helpers::generate_dummy_client_with_spec_and_accounts;
|
||||||
use super::super::ValidatorSet;
|
use super::super::ValidatorSet;
|
||||||
@ -154,8 +155,9 @@ mod tests {
|
|||||||
let client = generate_dummy_client_with_spec_and_accounts(Spec::new_validator_contract, None);
|
let client = generate_dummy_client_with_spec_and_accounts(Spec::new_validator_contract, None);
|
||||||
let vc = Arc::new(ValidatorContract::new(Address::from_str("0000000000000000000000000000000000000005").unwrap()));
|
let vc = Arc::new(ValidatorContract::new(Address::from_str("0000000000000000000000000000000000000005").unwrap()));
|
||||||
vc.register_contract(Arc::downgrade(&client));
|
vc.register_contract(Arc::downgrade(&client));
|
||||||
assert!(vc.contains(&Address::from_str("7d577a597b2742b498cb5cf0c26cdcd726d39e6e").unwrap()));
|
let last_hash = client.best_block_header().hash();
|
||||||
assert!(vc.contains(&Address::from_str("82a978b3f5962a5b0957d9ee9eef472ee55b42f1").unwrap()));
|
assert!(vc.contains(&last_hash, &Address::from_str("7d577a597b2742b498cb5cf0c26cdcd726d39e6e").unwrap()));
|
||||||
|
assert!(vc.contains(&last_hash, &Address::from_str("82a978b3f5962a5b0957d9ee9eef472ee55b42f1").unwrap()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -171,18 +173,21 @@ mod tests {
|
|||||||
|
|
||||||
client.miner().set_engine_signer(v1, "".into()).unwrap();
|
client.miner().set_engine_signer(v1, "".into()).unwrap();
|
||||||
let mut header = Header::default();
|
let mut header = Header::default();
|
||||||
let seal = encode(&vec!(5u8)).to_vec();
|
let seal = vec![encode(&5u8).to_vec(), encode(&(&H520::default() as &[u8])).to_vec()];
|
||||||
header.set_seal(vec!(seal));
|
header.set_seal(seal);
|
||||||
header.set_author(v1);
|
header.set_author(v1);
|
||||||
header.set_number(1);
|
header.set_number(2);
|
||||||
|
header.set_parent_hash(client.chain_info().best_block_hash);
|
||||||
|
|
||||||
// `reportBenign` when the designated proposer releases block from the future (bad clock).
|
// `reportBenign` when the designated proposer releases block from the future (bad clock).
|
||||||
assert!(client.engine().verify_block_unordered(&header, None).is_err());
|
assert!(client.engine().verify_block_family(&header, &header, None).is_err());
|
||||||
// Seal a block.
|
// Seal a block.
|
||||||
client.engine().step();
|
client.engine().step();
|
||||||
assert_eq!(client.chain_info().best_block_number, 1);
|
assert_eq!(client.chain_info().best_block_number, 1);
|
||||||
// Check if the unresponsive validator is `disliked`.
|
// Check if the unresponsive validator is `disliked`.
|
||||||
assert_eq!(client.call_contract(validator_contract, "d8f2e0bf".from_hex().unwrap()).unwrap().to_hex(), "0000000000000000000000007d577a597b2742b498cb5cf0c26cdcd726d39e6e");
|
assert_eq!(client.call_contract(BlockId::Latest, validator_contract, "d8f2e0bf".from_hex().unwrap()).unwrap().to_hex(), "0000000000000000000000007d577a597b2742b498cb5cf0c26cdcd726d39e6e");
|
||||||
// Simulate a misbehaving validator by handling a double proposal.
|
// Simulate a misbehaving validator by handling a double proposal.
|
||||||
|
let header = client.best_block_header().decode();
|
||||||
assert!(client.engine().verify_block_family(&header, &header, None).is_err());
|
assert!(client.engine().verify_block_family(&header, &header, None).is_err());
|
||||||
// Seal a block.
|
// Seal a block.
|
||||||
client.engine().step();
|
client.engine().step();
|
||||||
|
@ -21,7 +21,7 @@ mod safe_contract;
|
|||||||
mod contract;
|
mod contract;
|
||||||
|
|
||||||
use std::sync::Weak;
|
use std::sync::Weak;
|
||||||
use util::{Address, Arc};
|
use util::{Address, H256};
|
||||||
use ethjson::spec::ValidatorSet as ValidatorSpec;
|
use ethjson::spec::ValidatorSet as ValidatorSpec;
|
||||||
use client::Client;
|
use client::Client;
|
||||||
use self::simple_list::SimpleList;
|
use self::simple_list::SimpleList;
|
||||||
@ -32,18 +32,18 @@ use self::safe_contract::ValidatorSafeContract;
|
|||||||
pub fn new_validator_set(spec: ValidatorSpec) -> Box<ValidatorSet + Send + Sync> {
|
pub fn new_validator_set(spec: ValidatorSpec) -> Box<ValidatorSet + Send + Sync> {
|
||||||
match spec {
|
match spec {
|
||||||
ValidatorSpec::List(list) => Box::new(SimpleList::new(list.into_iter().map(Into::into).collect())),
|
ValidatorSpec::List(list) => Box::new(SimpleList::new(list.into_iter().map(Into::into).collect())),
|
||||||
ValidatorSpec::SafeContract(address) => Box::new(Arc::new(ValidatorSafeContract::new(address.into()))),
|
ValidatorSpec::SafeContract(address) => Box::new(ValidatorSafeContract::new(address.into())),
|
||||||
ValidatorSpec::Contract(address) => Box::new(Arc::new(ValidatorContract::new(address.into()))),
|
ValidatorSpec::Contract(address) => Box::new(ValidatorContract::new(address.into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ValidatorSet {
|
pub trait ValidatorSet {
|
||||||
/// Checks if a given address is a validator.
|
/// Checks if a given address is a validator.
|
||||||
fn contains(&self, address: &Address) -> bool;
|
fn contains(&self, bh: &H256, address: &Address) -> bool;
|
||||||
/// Draws an validator nonce modulo number of validators.
|
/// Draws an validator nonce modulo number of validators.
|
||||||
fn get(&self, nonce: usize) -> Address;
|
fn get(&self, bh: &H256, nonce: usize) -> Address;
|
||||||
/// Returns the current number of validators.
|
/// Returns the current number of validators.
|
||||||
fn count(&self) -> usize;
|
fn count(&self, bh: &H256) -> usize;
|
||||||
/// Notifies about malicious behaviour.
|
/// Notifies about malicious behaviour.
|
||||||
fn report_malicious(&self, _validator: &Address) {}
|
fn report_malicious(&self, _validator: &Address) {}
|
||||||
/// Notifies about benign misbehaviour.
|
/// Notifies about benign misbehaviour.
|
||||||
|
@ -17,17 +17,23 @@
|
|||||||
/// Validator set maintained in a contract, updated using `getValidators` method.
|
/// Validator set maintained in a contract, updated using `getValidators` method.
|
||||||
|
|
||||||
use std::sync::Weak;
|
use std::sync::Weak;
|
||||||
|
use ethabi;
|
||||||
use util::*;
|
use util::*;
|
||||||
|
use util::cache::MemoryLruCache;
|
||||||
|
use types::ids::BlockId;
|
||||||
use client::{Client, BlockChainClient};
|
use client::{Client, BlockChainClient};
|
||||||
use client::chain_notify::ChainNotify;
|
|
||||||
use super::ValidatorSet;
|
use super::ValidatorSet;
|
||||||
use super::simple_list::SimpleList;
|
use super::simple_list::SimpleList;
|
||||||
|
|
||||||
|
const MEMOIZE_CAPACITY: usize = 500;
|
||||||
|
const CONTRACT_INTERFACE: &'static [u8] = b"[{\"constant\":true,\"inputs\":[],\"name\":\"getValidators\",\"outputs\":[{\"name\":\"\",\"type\":\"address[]\"}],\"payable\":false,\"type\":\"function\"}]";
|
||||||
|
const GET_VALIDATORS: &'static str = "getValidators";
|
||||||
|
|
||||||
/// The validator contract should have the following interface:
|
/// The validator contract should have the following interface:
|
||||||
/// [{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}]
|
/// [{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}]
|
||||||
pub struct ValidatorSafeContract {
|
pub struct ValidatorSafeContract {
|
||||||
pub address: Address,
|
pub address: Address,
|
||||||
validators: RwLock<SimpleList>,
|
validators: RwLock<MemoryLruCache<H256, SimpleList>>,
|
||||||
provider: RwLock<Option<provider::Contract>>,
|
provider: RwLock<Option<provider::Contract>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,102 +41,127 @@ impl ValidatorSafeContract {
|
|||||||
pub fn new(contract_address: Address) -> Self {
|
pub fn new(contract_address: Address) -> Self {
|
||||||
ValidatorSafeContract {
|
ValidatorSafeContract {
|
||||||
address: contract_address,
|
address: contract_address,
|
||||||
validators: Default::default(),
|
validators: RwLock::new(MemoryLruCache::new(MEMOIZE_CAPACITY)),
|
||||||
provider: RwLock::new(None),
|
provider: RwLock::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queries the state and updates the set of validators.
|
/// Queries the state and gets the set of validators.
|
||||||
pub fn update(&self) {
|
fn get_list(&self, block_hash: H256) -> Option<SimpleList> {
|
||||||
if let Some(ref provider) = *self.provider.read() {
|
if let Some(ref provider) = *self.provider.read() {
|
||||||
match provider.get_validators() {
|
match provider.get_validators(BlockId::Hash(block_hash)) {
|
||||||
Ok(new) => {
|
Ok(new) => {
|
||||||
debug!(target: "engine", "Set of validators obtained: {:?}", new);
|
debug!(target: "engine", "Set of validators obtained: {:?}", new);
|
||||||
*self.validators.write() = SimpleList::new(new);
|
Some(SimpleList::new(new))
|
||||||
|
},
|
||||||
|
Err(s) => {
|
||||||
|
debug!(target: "engine", "Set of validators could not be updated: {}", s);
|
||||||
|
None
|
||||||
},
|
},
|
||||||
Err(s) => warn!(target: "engine", "Set of validators could not be updated: {}", s),
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!(target: "engine", "Set of validators could not be updated: no provider contract.")
|
warn!(target: "engine", "Set of validators could not be updated: no provider contract.");
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks validators on every block.
|
impl ValidatorSet for ValidatorSafeContract {
|
||||||
impl ChainNotify for ValidatorSafeContract {
|
fn contains(&self, block_hash: &H256, address: &Address) -> bool {
|
||||||
fn new_blocks(
|
let mut guard = self.validators.write();
|
||||||
&self,
|
let maybe_existing = guard
|
||||||
_: Vec<H256>,
|
.get_mut(block_hash)
|
||||||
_: Vec<H256>,
|
.map(|list| list.contains(block_hash, address));
|
||||||
enacted: Vec<H256>,
|
maybe_existing
|
||||||
_: Vec<H256>,
|
.unwrap_or_else(|| self
|
||||||
_: Vec<H256>,
|
.get_list(block_hash.clone())
|
||||||
_: Vec<Bytes>,
|
.map_or(false, |list| {
|
||||||
_duration: u64) {
|
let contains = list.contains(block_hash, address);
|
||||||
if !enacted.is_empty() {
|
guard.insert(block_hash.clone(), list);
|
||||||
self.update();
|
contains
|
||||||
}
|
}))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ValidatorSet for Arc<ValidatorSafeContract> {
|
|
||||||
fn contains(&self, address: &Address) -> bool {
|
|
||||||
self.validators.read().contains(address)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, nonce: usize) -> Address {
|
fn get(&self, block_hash: &H256, nonce: usize) -> Address {
|
||||||
self.validators.read().get(nonce)
|
let mut guard = self.validators.write();
|
||||||
|
let maybe_existing = guard
|
||||||
|
.get_mut(block_hash)
|
||||||
|
.map(|list| list.get(block_hash, nonce));
|
||||||
|
maybe_existing
|
||||||
|
.unwrap_or_else(|| self
|
||||||
|
.get_list(block_hash.clone())
|
||||||
|
.map_or_else(Default::default, |list| {
|
||||||
|
let address = list.get(block_hash, nonce);
|
||||||
|
guard.insert(block_hash.clone(), list);
|
||||||
|
address
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn count(&self) -> usize {
|
fn count(&self, block_hash: &H256) -> usize {
|
||||||
self.validators.read().count()
|
let mut guard = self.validators.write();
|
||||||
|
let maybe_existing = guard
|
||||||
|
.get_mut(block_hash)
|
||||||
|
.map(|list| list.count(block_hash));
|
||||||
|
maybe_existing
|
||||||
|
.unwrap_or_else(|| self
|
||||||
|
.get_list(block_hash.clone())
|
||||||
|
.map_or_else(usize::max_value, |list| {
|
||||||
|
let address = list.count(block_hash);
|
||||||
|
guard.insert(block_hash.clone(), list);
|
||||||
|
address
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_contract(&self, client: Weak<Client>) {
|
fn register_contract(&self, client: Weak<Client>) {
|
||||||
if let Some(c) = client.upgrade() {
|
trace!(target: "engine", "Setting up contract caller.");
|
||||||
c.add_notify(self.clone());
|
let contract = ethabi::Contract::new(ethabi::Interface::load(CONTRACT_INTERFACE).expect("JSON interface is valid; qed"));
|
||||||
}
|
let call = contract.function(GET_VALIDATORS.into()).expect("Method name is valid; qed");
|
||||||
{
|
let data = call.encode_call(vec![]).expect("get_validators does not take any arguments; qed");
|
||||||
*self.provider.write() = Some(provider::Contract::new(self.address, move |a, d| client.upgrade().ok_or("No client!".into()).and_then(|c| c.call_contract(a, d))));
|
let contract_address = self.address.clone();
|
||||||
}
|
let do_call = move |id| client
|
||||||
self.update();
|
.upgrade()
|
||||||
|
.ok_or("No client!".into())
|
||||||
|
.and_then(|c| c.call_contract(id, contract_address.clone(), data.clone()))
|
||||||
|
.map(|raw_output| call.decode_output(raw_output).expect("ethabi is correct; qed"));
|
||||||
|
*self.provider.write() = Some(provider::Contract::new(do_call));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod provider {
|
mod provider {
|
||||||
// Autogenerated from JSON contract definition using Rust contract convertor.
|
|
||||||
#![allow(unused_imports)]
|
|
||||||
use std::string::String;
|
use std::string::String;
|
||||||
use std::result::Result;
|
use std::result::Result;
|
||||||
use std::fmt;
|
|
||||||
use {util, ethabi};
|
use {util, ethabi};
|
||||||
use util::{FixedHash, Uint};
|
use types::ids::BlockId;
|
||||||
|
|
||||||
pub struct Contract {
|
pub struct Contract {
|
||||||
contract: ethabi::Contract,
|
do_call: Box<Fn(BlockId) -> Result<Vec<ethabi::Token>, String> + Send + Sync + 'static>,
|
||||||
address: util::Address,
|
|
||||||
do_call: Box<Fn(util::Address, Vec<u8>) -> Result<Vec<u8>, String> + Send + Sync + 'static>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Contract {
|
impl Contract {
|
||||||
pub fn new<F>(address: util::Address, do_call: F) -> Self where F: Fn(util::Address, Vec<u8>) -> Result<Vec<u8>, String> + Send + Sync + 'static {
|
pub fn new<F>(do_call: F) -> Self where F: Fn(BlockId) -> Result<Vec<ethabi::Token>, String> + Send + Sync + 'static {
|
||||||
Contract {
|
Contract {
|
||||||
contract: ethabi::Contract::new(ethabi::Interface::load(b"[{\"constant\":true,\"inputs\":[],\"name\":\"getValidators\",\"outputs\":[{\"name\":\"\",\"type\":\"address[]\"}],\"payable\":false,\"type\":\"function\"}]").expect("JSON is autogenerated; qed")),
|
|
||||||
address: address,
|
|
||||||
do_call: Box::new(do_call),
|
do_call: Box::new(do_call),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn as_string<T: fmt::Debug>(e: T) -> String { format!("{:?}", e) }
|
|
||||||
|
|
||||||
/// Auto-generated from: `{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}`
|
/// Gets validators from contract with interface: `{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"type":"function"}`
|
||||||
#[allow(dead_code)]
|
pub fn get_validators(&self, id: BlockId) -> Result<Vec<util::Address>, String> {
|
||||||
pub fn get_validators(&self) -> Result<Vec<util::Address>, String> {
|
Ok((self.do_call)(id)?
|
||||||
let call = self.contract.function("getValidators".into()).map_err(Self::as_string)?;
|
.into_iter()
|
||||||
let data = call.encode_call(
|
.rev()
|
||||||
vec![]
|
.collect::<Vec<_>>()
|
||||||
).map_err(Self::as_string)?;
|
.pop()
|
||||||
let output = call.decode_output((self.do_call)(self.address.clone(), data)?).map_err(Self::as_string)?;
|
.expect("get_validators returns one argument; qed")
|
||||||
let mut result = output.into_iter().rev().collect::<Vec<_>>();
|
.to_array()
|
||||||
Ok(({ let r = result.pop().ok_or("Invalid return arity")?; let r = r.to_array().and_then(|v| v.into_iter().map(|a| a.to_address()).collect::<Option<Vec<[u8; 20]>>>()).ok_or("Invalid type returned")?; r.into_iter().map(|a| util::Address::from(a)).collect::<Vec<_>>() }))
|
.and_then(|v| v
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| a.to_address())
|
||||||
|
.collect::<Option<Vec<[u8; 20]>>>())
|
||||||
|
.expect("get_validators returns a list of addresses; qed")
|
||||||
|
.into_iter()
|
||||||
|
.map(util::Address::from)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,13 +169,14 @@ mod provider {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use util::*;
|
use util::*;
|
||||||
|
use types::ids::BlockId;
|
||||||
use spec::Spec;
|
use spec::Spec;
|
||||||
use account_provider::AccountProvider;
|
use account_provider::AccountProvider;
|
||||||
use transaction::{Transaction, Action};
|
use transaction::{Transaction, Action};
|
||||||
use client::{BlockChainClient, EngineClient};
|
use client::{BlockChainClient, EngineClient};
|
||||||
use ethkey::Secret;
|
use ethkey::Secret;
|
||||||
use miner::MinerService;
|
use miner::MinerService;
|
||||||
use tests::helpers::generate_dummy_client_with_spec_and_accounts;
|
use tests::helpers::{generate_dummy_client_with_spec_and_accounts, generate_dummy_client_with_spec_and_data};
|
||||||
use super::super::ValidatorSet;
|
use super::super::ValidatorSet;
|
||||||
use super::ValidatorSafeContract;
|
use super::ValidatorSafeContract;
|
||||||
|
|
||||||
@ -153,12 +185,13 @@ mod tests {
|
|||||||
let client = generate_dummy_client_with_spec_and_accounts(Spec::new_validator_safe_contract, None);
|
let client = generate_dummy_client_with_spec_and_accounts(Spec::new_validator_safe_contract, None);
|
||||||
let vc = Arc::new(ValidatorSafeContract::new(Address::from_str("0000000000000000000000000000000000000005").unwrap()));
|
let vc = Arc::new(ValidatorSafeContract::new(Address::from_str("0000000000000000000000000000000000000005").unwrap()));
|
||||||
vc.register_contract(Arc::downgrade(&client));
|
vc.register_contract(Arc::downgrade(&client));
|
||||||
assert!(vc.contains(&Address::from_str("7d577a597b2742b498cb5cf0c26cdcd726d39e6e").unwrap()));
|
let last_hash = client.best_block_header().hash();
|
||||||
assert!(vc.contains(&Address::from_str("82a978b3f5962a5b0957d9ee9eef472ee55b42f1").unwrap()));
|
assert!(vc.contains(&last_hash, &Address::from_str("7d577a597b2742b498cb5cf0c26cdcd726d39e6e").unwrap()));
|
||||||
|
assert!(vc.contains(&last_hash, &Address::from_str("82a978b3f5962a5b0957d9ee9eef472ee55b42f1").unwrap()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn updates_validators() {
|
fn knows_validators() {
|
||||||
let tap = Arc::new(AccountProvider::transient_provider());
|
let tap = Arc::new(AccountProvider::transient_provider());
|
||||||
let s0 = Secret::from_slice(&"1".sha3()).unwrap();
|
let s0 = Secret::from_slice(&"1".sha3()).unwrap();
|
||||||
let v0 = tap.insert_account(s0.clone(), "").unwrap();
|
let v0 = tap.insert_account(s0.clone(), "").unwrap();
|
||||||
@ -212,5 +245,14 @@ mod tests {
|
|||||||
client.update_sealing();
|
client.update_sealing();
|
||||||
// Able to seal again.
|
// Able to seal again.
|
||||||
assert_eq!(client.chain_info().best_block_number, 3);
|
assert_eq!(client.chain_info().best_block_number, 3);
|
||||||
|
|
||||||
|
// Check syncing.
|
||||||
|
let sync_client = generate_dummy_client_with_spec_and_data(Spec::new_validator_safe_contract, 0, 0, &[]);
|
||||||
|
sync_client.engine().register_client(Arc::downgrade(&sync_client));
|
||||||
|
for i in 1..4 {
|
||||||
|
sync_client.import_block(client.block(BlockId::Number(i)).unwrap().into_inner()).unwrap();
|
||||||
|
}
|
||||||
|
sync_client.flush_queue();
|
||||||
|
assert_eq!(sync_client.chain_info().best_block_number, 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
/// Preconfigured validator list.
|
/// Preconfigured validator list.
|
||||||
|
|
||||||
use util::Address;
|
use util::{H256, Address, HeapSizeOf};
|
||||||
use super::ValidatorSet;
|
use super::ValidatorSet;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Default)]
|
#[derive(Debug, PartialEq, Eq, Default)]
|
||||||
@ -34,16 +34,22 @@ impl SimpleList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HeapSizeOf for SimpleList {
|
||||||
|
fn heap_size_of_children(&self) -> usize {
|
||||||
|
self.validators.heap_size_of_children() + self.validator_n.heap_size_of_children()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ValidatorSet for SimpleList {
|
impl ValidatorSet for SimpleList {
|
||||||
fn contains(&self, address: &Address) -> bool {
|
fn contains(&self, _bh: &H256, address: &Address) -> bool {
|
||||||
self.validators.contains(address)
|
self.validators.contains(address)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, nonce: usize) -> Address {
|
fn get(&self, _bh: &H256, nonce: usize) -> Address {
|
||||||
self.validators.get(nonce % self.validator_n).expect("There are validator_n authorities; taking number modulo validator_n gives number in validator_n range; qed").clone()
|
self.validators.get(nonce % self.validator_n).expect("There are validator_n authorities; taking number modulo validator_n gives number in validator_n range; qed").clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn count(&self) -> usize {
|
fn count(&self, _bh: &H256) -> usize {
|
||||||
self.validator_n
|
self.validator_n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,9 +66,9 @@ mod tests {
|
|||||||
let a1 = Address::from_str("cd1722f3947def4cf144679da39c4c32bdc35681").unwrap();
|
let a1 = Address::from_str("cd1722f3947def4cf144679da39c4c32bdc35681").unwrap();
|
||||||
let a2 = Address::from_str("0f572e5295c57f15886f9b263e2f6d2d6c7b5ec6").unwrap();
|
let a2 = Address::from_str("0f572e5295c57f15886f9b263e2f6d2d6c7b5ec6").unwrap();
|
||||||
let list = SimpleList::new(vec![a1.clone(), a2.clone()]);
|
let list = SimpleList::new(vec![a1.clone(), a2.clone()]);
|
||||||
assert!(list.contains(&a1));
|
assert!(list.contains(&Default::default(), &a1));
|
||||||
assert_eq!(list.get(0), a1);
|
assert_eq!(list.get(&Default::default(), 0), a1);
|
||||||
assert_eq!(list.get(1), a2);
|
assert_eq!(list.get(&Default::default(), 1), a2);
|
||||||
assert_eq!(list.get(2), a1);
|
assert_eq!(list.get(&Default::default(), 2), a1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use types::ids::BlockId;
|
||||||
use client::MiningBlockChainClient;
|
use client::MiningBlockChainClient;
|
||||||
use transaction::SignedTransaction;
|
use transaction::SignedTransaction;
|
||||||
use util::{U256, Uint, Mutex};
|
use util::{U256, Uint, Mutex};
|
||||||
@ -45,7 +46,7 @@ impl ServiceTransactionChecker {
|
|||||||
debug_assert_eq!(tx.gas_price, U256::zero());
|
debug_assert_eq!(tx.gas_price, U256::zero());
|
||||||
|
|
||||||
if let Some(ref contract) = *self.contract.lock() {
|
if let Some(ref contract) = *self.contract.lock() {
|
||||||
let do_call = |a, d| client.call_contract(a, d);
|
let do_call = |a, d| client.call_contract(BlockId::Latest, a, d);
|
||||||
contract.certified(&do_call, &tx.sender())
|
contract.certified(&do_call, &tx.sender())
|
||||||
} else {
|
} else {
|
||||||
Err("contract is not configured".to_owned())
|
Err("contract is not configured".to_owned())
|
||||||
|
@ -247,7 +247,7 @@ impl Updater {
|
|||||||
if let Some(ops_addr) = self.client.upgrade().and_then(|c| c.registry_address("operations".into())) {
|
if let Some(ops_addr) = self.client.upgrade().and_then(|c| c.registry_address("operations".into())) {
|
||||||
trace!(target: "updater", "Found operations at {}", ops_addr);
|
trace!(target: "updater", "Found operations at {}", ops_addr);
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
*self.operations.lock() = Some(Operations::new(ops_addr, move |a, d| client.upgrade().ok_or("No client!".into()).and_then(|c| c.call_contract(a, d))));
|
*self.operations.lock() = Some(Operations::new(ops_addr, move |a, d| client.upgrade().ok_or("No client!".into()).and_then(|c| c.call_contract(BlockId::Latest, a, d))));
|
||||||
} else {
|
} else {
|
||||||
// No Operations contract - bail.
|
// No Operations contract - bail.
|
||||||
return;
|
return;
|
||||||
@ -340,7 +340,7 @@ impl fetch::urlhint::ContractClient for Updater {
|
|||||||
|
|
||||||
fn call(&self, address: Address, data: Bytes) -> Result<Bytes, String> {
|
fn call(&self, address: Address, data: Bytes) -> Result<Bytes, String> {
|
||||||
self.client.upgrade().ok_or_else(|| "Client not available".to_owned())?
|
self.client.upgrade().ok_or_else(|| "Client not available".to_owned())?
|
||||||
.call_contract(address, data)
|
.call_contract(BlockId::Latest, address, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user