This repository has been archived by the owner on Nov 6, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Transaction Queue banning #2524
Merged
Merged
Changes from 4 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
3eb8ef8
Blacklisting transaction queue
eb9d58b
Using blacklisting queue in miner
1810e44
Restoring todo [ci:skip]
133c899
Merge branch 'master' into queue-blacklist
ad06276
Blacklisting recipients and code
48a9e71
Renaming blacklisting->banning
77e4ac5
CLI option for banning.
e4fec05
Fixing submodule commit [ci:skip]
58b4a62
Fixing RPC tests
eb6c863
Additional logging when dropping transactions
9dbe3c0
whitespace
gavofyork 068b759
Configurable ban duration
9c96e55
Reverting fix for pruning history from config file
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,335 @@ | ||
// Copyright 2015, 2016 Ethcore (UK) Ltd. | ||
// This file is part of Parity. | ||
|
||
// Parity is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
|
||
// Parity is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
|
||
// You should have received a copy of the GNU General Public License | ||
// along with Parity. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
//! Banning Queue | ||
//! Transacton Queue wrapper maintaining additional list of banned senders and contract hashes. | ||
|
||
use std::ops::{Deref, DerefMut}; | ||
use std::cell::Cell; | ||
use transaction::{SignedTransaction, Action}; | ||
use transient_hashmap::TransientHashMap; | ||
use miner::{Banning, TransactionQueue, TransactionImportResult, TransactionOrigin, AccountDetails}; | ||
use error::{Error, TransactionError}; | ||
use util::{Uint, U256, H256, Address, Hashable}; | ||
|
||
type Count = u16; | ||
const BAN_LIFETIME_SEC: u64 = 180; | ||
|
||
/// Auto-Banning threshold | ||
pub enum Threshold { | ||
/// Should ban after given number of misbehaves reported. | ||
BanAfter(Count), | ||
/// Should never ban anything | ||
NeverBan | ||
} | ||
|
||
impl From<Banning> for Threshold { | ||
fn from(banning: Banning) -> Self { | ||
match banning { | ||
Banning::Disabled => Threshold::NeverBan, | ||
Banning::Enabled { min_offends, .. } => Threshold::BanAfter(min_offends), | ||
} | ||
} | ||
} | ||
|
||
impl Default for Threshold { | ||
fn default() -> Self { | ||
Threshold::NeverBan | ||
} | ||
} | ||
|
||
/// Transaction queue with banlist. | ||
pub struct BanningTransactionQueue { | ||
queue: TransactionQueue, | ||
ban_threshold: Threshold, | ||
senders_bans: TransientHashMap<Address, Cell<Count>>, | ||
recipients_bans: TransientHashMap<Address, Cell<Count>>, | ||
codes_bans: TransientHashMap<H256, Cell<Count>>, | ||
} | ||
|
||
impl BanningTransactionQueue { | ||
/// Creates new banlisting transaction queue | ||
pub fn new(queue: TransactionQueue, ban_threshold: Threshold) -> Self { | ||
BanningTransactionQueue { | ||
queue: queue, | ||
ban_threshold: ban_threshold, | ||
senders_bans: TransientHashMap::new(BAN_LIFETIME_SEC), | ||
recipients_bans: TransientHashMap::new(BAN_LIFETIME_SEC), | ||
codes_bans: TransientHashMap::new(BAN_LIFETIME_SEC), | ||
} | ||
} | ||
|
||
/// Borrows internal queue. | ||
/// NOTE: you can insert transactions to the queue even | ||
/// if they would be rejected because of ban otherwise. | ||
/// But probably you shouldn't. | ||
pub fn queue(&mut self) -> &mut TransactionQueue { | ||
&mut self.queue | ||
} | ||
|
||
/// Add to the queue taking bans into consideration. | ||
/// May reject transaction because of the banlist. | ||
pub fn add_with_banlist<F>( | ||
&mut self, | ||
transaction: SignedTransaction, | ||
account_details: &F, | ||
) -> Result<TransactionImportResult, Error> | ||
where F: Fn(&Address) -> AccountDetails { | ||
if let Threshold::BanAfter(threshold) = self.ban_threshold { | ||
// NOTE In all checks use direct query to avoid increasing ban timeout. | ||
|
||
// Check sender | ||
if let Ok(sender) = transaction.sender() { | ||
let count = self.senders_bans.direct().get(&sender).map(|v| v.get()).unwrap_or(0); | ||
if count > threshold { | ||
return Err(Error::Transaction(TransactionError::SenderBanned)); | ||
} | ||
} | ||
|
||
// Check recipient | ||
if let Action::Call(recipient) = transaction.action { | ||
let count = self.recipients_bans.direct().get(&recipient).map(|v| v.get()).unwrap_or(0); | ||
if count > threshold { | ||
return Err(Error::Transaction(TransactionError::RecipientBanned)); | ||
} | ||
} | ||
|
||
// Check code | ||
if let Action::Create = transaction.action { | ||
let code_hash = transaction.data.sha3(); | ||
let count = self.codes_bans.direct().get(&code_hash).map(|v| v.get()).unwrap_or(0); | ||
if count > threshold { | ||
return Err(Error::Transaction(TransactionError::CodeBanned)); | ||
} | ||
} | ||
} | ||
self.queue.add(transaction, account_details, TransactionOrigin::External) | ||
} | ||
|
||
/// Ban transaction with given hash. | ||
/// Transaction has to be in the queue. | ||
/// | ||
/// Bans sender and recipient/code and returns `true` when any ban has reached threshold. | ||
pub fn ban_transaction(&mut self, hash: &H256) -> bool { | ||
let transaction = self.queue.find(hash); | ||
match transaction { | ||
Some(transaction) => { | ||
let sender = transaction.sender().expect("Transaction is in queue, so the sender is already validated; qed"); | ||
// Ban sender | ||
let sender_banned = self.ban_sender(sender); | ||
// Ban recipient and codehash | ||
let is_banned = sender_banned || match transaction.action { | ||
Action::Call(recipient) => { | ||
self.ban_recipient(recipient) | ||
}, | ||
Action::Create => { | ||
self.ban_codehash(transaction.data.sha3()) | ||
}, | ||
}; | ||
is_banned | ||
}, | ||
None => false, | ||
} | ||
} | ||
|
||
/// Ban given sender. | ||
/// If bans threshold is reached all subsequent transactions from this sender will be rejected. | ||
/// Reaching bans threshold also removes all existsing transaction from this sender that are already in the | ||
/// queue. | ||
fn ban_sender(&mut self, address: Address) -> bool { | ||
let count = { | ||
let mut count = self.senders_bans.entry(address).or_insert_with(|| Cell::new(0)); | ||
*count.get_mut() = count.get().saturating_add(1); | ||
count.get() | ||
}; | ||
match self.ban_threshold { | ||
Threshold::BanAfter(threshold) if count > threshold => { | ||
// Banlist the sender. | ||
// Remove all transactions from the queue. | ||
self.remove_all(address, !U256::zero()); | ||
true | ||
}, | ||
_ => false | ||
} | ||
} | ||
|
||
/// Ban given recipient. | ||
/// If bans threshold is reached all subsequent transactions to this address will be rejected. | ||
/// Returns true if bans threshold has been reached. | ||
fn ban_recipient(&mut self, address: Address) -> bool { | ||
let count = { | ||
let mut count = self.recipients_bans.entry(address).or_insert_with(|| Cell::new(0)); | ||
*count.get_mut() = count.get().saturating_add(1); | ||
count.get() | ||
}; | ||
match self.ban_threshold { | ||
// TODO [ToDr] Consider removing other transactions to the same recipient from the queue? | ||
Threshold::BanAfter(threshold) if count > threshold => true, | ||
_ => false | ||
} | ||
} | ||
|
||
|
||
/// Ban given codehash. | ||
/// If bans threshold is reached all subsequent transactions to contracts with this codehash will be rejected. | ||
/// Returns true if bans threshold has been reached. | ||
fn ban_codehash(&mut self, code_hash: H256) -> bool { | ||
let mut count = self.codes_bans.entry(code_hash).or_insert_with(|| Cell::new(0)); | ||
*count.get_mut() = count.get().saturating_add(1); | ||
|
||
match self.ban_threshold { | ||
// TODO [ToDr] Consider removing other transactions with the same code from the queue? | ||
Threshold::BanAfter(threshold) if count.get() > threshold => true, | ||
_ => false, | ||
} | ||
} | ||
} | ||
|
||
impl Deref for BanningTransactionQueue { | ||
type Target = TransactionQueue; | ||
|
||
fn deref(&self) -> &Self::Target { | ||
&self.queue | ||
} | ||
} | ||
impl DerefMut for BanningTransactionQueue { | ||
fn deref_mut(&mut self) -> &mut Self::Target { | ||
self.queue() | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::{BanningTransactionQueue, Threshold}; | ||
use ethkey::{Random, Generator}; | ||
use transaction::{Transaction, SignedTransaction, Action}; | ||
use error::{Error, TransactionError}; | ||
use client::TransactionImportResult; | ||
use miner::{TransactionQueue, TransactionOrigin, AccountDetails}; | ||
use util::{Uint, U256, Address, FromHex, Hashable}; | ||
|
||
fn queue() -> BanningTransactionQueue { | ||
BanningTransactionQueue::new(TransactionQueue::default(), Threshold::BanAfter(1)) | ||
} | ||
|
||
fn default_account_details(_address: &Address) -> AccountDetails { | ||
AccountDetails { | ||
nonce: U256::zero(), | ||
balance: !U256::zero(), | ||
} | ||
} | ||
|
||
fn transaction(action: Action) -> SignedTransaction { | ||
let keypair = Random.generate().unwrap(); | ||
Transaction { | ||
action: action, | ||
value: U256::from(100), | ||
data: "3331600055".from_hex().unwrap(), | ||
gas: U256::from(100_000), | ||
gas_price: U256::from(10), | ||
nonce: U256::from(0), | ||
}.sign(keypair.secret()) | ||
} | ||
|
||
fn unwrap_err(res: Result<TransactionImportResult, Error>) -> TransactionError { | ||
match res { | ||
Err(Error::Transaction(e)) => e, | ||
Ok(x) => panic!("Expected error, got: Ok({:?})", x), | ||
Err(e) => panic!("Unexpected error type returned by queue: {:?}", e), | ||
} | ||
} | ||
|
||
#[test] | ||
fn should_allow_to_borrow_the_queue() { | ||
// given | ||
let tx = transaction(Action::Create); | ||
let mut txq = queue(); | ||
|
||
// when | ||
txq.queue().add(tx, &default_account_details, TransactionOrigin::External).unwrap(); | ||
|
||
// then | ||
// should also deref to queue | ||
assert_eq!(txq.status().pending, 1); | ||
} | ||
|
||
#[test] | ||
fn should_not_accept_transactions_from_banned_sender() { | ||
// given | ||
let tx = transaction(Action::Create); | ||
let mut txq = queue(); | ||
// Banlist once (threshold not reached) | ||
let banlist1 = txq.ban_sender(tx.sender().unwrap()); | ||
assert!(!banlist1, "Threshold not reached yet."); | ||
// Insert once | ||
let import1 = txq.add_with_banlist(tx.clone(), &default_account_details).unwrap(); | ||
assert_eq!(import1, TransactionImportResult::Current); | ||
|
||
// when | ||
let banlist2 = txq.ban_sender(tx.sender().unwrap()); | ||
let import2 = txq.add_with_banlist(tx.clone(), &default_account_details); | ||
|
||
// then | ||
assert!(banlist2, "Threshold should be reached - banned."); | ||
assert_eq!(unwrap_err(import2), TransactionError::SenderBanned); | ||
// Should also remove transacion from the queue | ||
assert_eq!(txq.find(&tx.hash()), None); | ||
} | ||
|
||
#[test] | ||
fn should_not_accept_transactions_to_banned_recipient() { | ||
// given | ||
let recipient = Address::default(); | ||
let tx = transaction(Action::Call(recipient)); | ||
let mut txq = queue(); | ||
// Banlist once (threshold not reached) | ||
let banlist1 = txq.ban_recipient(recipient); | ||
assert!(!banlist1, "Threshold not reached yet."); | ||
// Insert once | ||
let import1 = txq.add_with_banlist(tx.clone(), &default_account_details).unwrap(); | ||
assert_eq!(import1, TransactionImportResult::Current); | ||
|
||
// when | ||
let banlist2 = txq.ban_recipient(recipient); | ||
let import2 = txq.add_with_banlist(tx.clone(), &default_account_details); | ||
|
||
// then | ||
assert!(banlist2, "Threshold should be reached - banned."); | ||
assert_eq!(unwrap_err(import2), TransactionError::RecipientBanned); | ||
} | ||
|
||
#[test] | ||
fn should_not_accept_transactions_with_banned_code() { | ||
// given | ||
let tx = transaction(Action::Create); | ||
let codehash = tx.data.sha3(); | ||
let mut txq = queue(); | ||
// Banlist once (threshold not reached) | ||
let banlist1 = txq.ban_codehash(codehash); | ||
assert!(!banlist1, "Threshold not reached yet."); | ||
// Insert once | ||
let import1 = txq.add_with_banlist(tx.clone(), &default_account_details).unwrap(); | ||
assert_eq!(import1, TransactionImportResult::Current); | ||
|
||
// when | ||
let banlist2 = txq.ban_codehash(codehash); | ||
let import2 = txq.add_with_banlist(tx.clone(), &default_account_details); | ||
|
||
// then | ||
assert!(banlist2, "Threshold should be reached - banned."); | ||
assert_eq!(unwrap_err(import2), TransactionError::CodeBanned); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why move to the direct version? i've found that they'll usually publish a new version if you ask :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either I broke something during merge or it was already like this on master:
https://github.com/ethcore/parity/blame/master/ethcore/Cargo.toml