Skip to content
This repository has been archived by the owner on Nov 6, 2020. It is now read-only.

Transaction Queue banning #2524

Merged
merged 13 commits into from
Oct 27, 2016
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ethcore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ semver = "0.2"
bit-set = "0.4"
time = "0.1"
rand = "0.3"
lru-cache = "0.0.7"
byteorder = "0.5"
transient-hashmap = "0.1"
lru-cache = { git = "https://github.com/contain-rs/lru-cache" }
Copy link
Contributor

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 :)

Copy link
Collaborator Author

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

evmjit = { path = "../evmjit", optional = true }
clippy = { version = "0.0.90", optional = true}
ethash = { path = "../ethash" }
Expand Down
2 changes: 1 addition & 1 deletion ethcore/res/ethereum/tests
Submodule tests updated 706 files
12 changes: 9 additions & 3 deletions ethcore/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,12 @@ pub enum TransactionError {
},
/// Transaction's gas limit (aka gas) is invalid.
InvalidGasLimit(OutOfBounds<U256>),
/// Transaction sender is blacklisted.
SenderBlacklisted,
/// Transaction sender is banned.
SenderBanned,
/// Transaction receipient is banned.
RecipientBanned,
/// Contract creation code is banned.
CodeBanned,
}

impl fmt::Display for TransactionError {
Expand All @@ -83,7 +87,9 @@ impl fmt::Display for TransactionError {
GasLimitExceeded { limit, got } =>
format!("Gas limit exceeded. Limit={}, Given={}", limit, got),
InvalidGasLimit(ref err) => format!("Invalid gas limit. {}", err),
SenderBlacklisted => "Sender is blacklisted.".into(),
SenderBanned => "Sender is temporarily banned.".into(),
RecipientBanned => "Recipient is temporarily banned.".into(),
CodeBanned => "Contract code is temporarily banned.".into(),
};

f.write_fmt(format_args!("Transaction error ({})", msg))
Expand Down
335 changes: 335 additions & 0 deletions ethcore/src/miner/banning_queue.rs
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);
}
}
Loading