diff --git a/ethcore/native_contracts/build.rs b/ethcore/native_contracts/build.rs index e7985b388..979535057 100644 --- a/ethcore/native_contracts/build.rs +++ b/ethcore/native_contracts/build.rs @@ -29,6 +29,7 @@ const SECRETSTORE_ACL_STORAGE_ABI: &'static str = include_str!("res/secretstore_ const VALIDATOR_SET_ABI: &'static str = include_str!("res/validator_set.json"); const VALIDATOR_REPORT_ABI: &'static str = include_str!("res/validator_report.json"); const PEER_SET_ABI: &'static str = include_str!("res/peer_set.json"); +const TX_ACL_ABI: &'static str = include_str!("res/tx_acl.json"); const TEST_VALIDATOR_SET_ABI: &'static str = include_str!("res/test_validator_set.json"); @@ -55,6 +56,7 @@ fn main() { build_file("ValidatorSet", VALIDATOR_SET_ABI, "validator_set.rs"); build_file("ValidatorReport", VALIDATOR_REPORT_ABI, "validator_report.rs"); build_file("PeerSet", PEER_SET_ABI, "peer_set.rs"); + build_file("TransactAcl", TX_ACL_ABI, "tx_acl.rs"); build_test_contracts(); } diff --git a/ethcore/native_contracts/generator/src/lib.rs b/ethcore/native_contracts/generator/src/lib.rs index 996ee4969..8de7555d2 100644 --- a/ethcore/native_contracts/generator/src/lib.rs +++ b/ethcore/native_contracts/generator/src/lib.rs @@ -302,7 +302,8 @@ fn detokenize(name: &str, output_type: ParamType) -> String { } ParamType::Uint(width) => { let read_uint = match width { - 8 | 16 | 32 | 64 => format!("bigint::prelude::U256(u).low_u64() as u{}", width), + 8 => "u[31] as u8".into(), + 16 | 32 | 64 => format!("BigEndian::read_u{}(&u[{}..])", width, 32 - (width / 8)), _ => format!("bigint::prelude::U{}::from(&u[..])", width), }; diff --git a/ethcore/native_contracts/res/tx_acl.json b/ethcore/native_contracts/res/tx_acl.json new file mode 100644 index 000000000..cff9956de --- /dev/null +++ b/ethcore/native_contracts/res/tx_acl.json @@ -0,0 +1 @@ +[{"constant":false,"inputs":[{"name":"sender","type":"address"}],"name":"allowedTxTypes","outputs":[{"name":"","type":"uint32"}],"payable":false,"stateMutability":"nonpayable","type":"function"}] diff --git a/ethcore/native_contracts/src/lib.rs b/ethcore/native_contracts/src/lib.rs index 733dea80e..c37a13504 100644 --- a/ethcore/native_contracts/src/lib.rs +++ b/ethcore/native_contracts/src/lib.rs @@ -31,6 +31,7 @@ mod secretstore_acl_storage; mod validator_set; mod validator_report; mod peer_set; +mod tx_acl; pub mod test_contracts; @@ -42,3 +43,4 @@ pub use self::secretstore_acl_storage::SecretStoreAclStorage; pub use self::validator_set::ValidatorSet; pub use self::validator_report::ValidatorReport; pub use self::peer_set::PeerSet; +pub use self::tx_acl::TransactAcl; diff --git a/ethcore/native_contracts/src/tx_acl.rs b/ethcore/native_contracts/src/tx_acl.rs new file mode 100644 index 000000000..1ab4c8e5d --- /dev/null +++ b/ethcore/native_contracts/src/tx_acl.rs @@ -0,0 +1,21 @@ +// Copyright 2015-2017 Parity Technologies (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 . + +#![allow(unused_mut, unused_variables, unused_imports)] + +//! Transact permissions contract. + +include!(concat!(env!("OUT_DIR"), "/tx_acl.rs")); diff --git a/ethcore/src/client/client.rs b/ethcore/src/client/client.rs index e749bb0ae..448188676 100644 --- a/ethcore/src/client/client.rs +++ b/ethcore/src/client/client.rs @@ -1446,6 +1446,10 @@ impl BlockChainClient for Client { self.state_at(id).and_then(|s| s.code(address).ok()).map(|c| c.map(|c| (&*c).clone())) } + fn code_hash(&self, address: &Address, id: BlockId) -> Option { + self.state_at(id).and_then(|s| s.code_hash(address).ok()) + } + fn balance(&self, address: &Address, id: BlockId) -> Option { self.state_at(id).and_then(|s| s.balance(address).ok()) } diff --git a/ethcore/src/client/test_client.rs b/ethcore/src/client/test_client.rs index da98175d6..0cd9fedc8 100644 --- a/ethcore/src/client/test_client.rs +++ b/ethcore/src/client/test_client.rs @@ -456,6 +456,13 @@ impl BlockChainClient for TestBlockChainClient { } } + fn code_hash(&self, address: &Address, id: BlockId) -> Option { + match id { + BlockId::Latest | BlockId::Pending => self.code.read().get(address).map(|c| keccak(&c)), + _ => None, + } + } + fn balance(&self, address: &Address, id: BlockId) -> Option { match id { BlockId::Latest | BlockId::Pending => Some(self.balances.read().get(address).cloned().unwrap_or_else(U256::zero)), diff --git a/ethcore/src/client/traits.rs b/ethcore/src/client/traits.rs index 5d049039b..45736e2c5 100644 --- a/ethcore/src/client/traits.rs +++ b/ethcore/src/client/traits.rs @@ -98,6 +98,9 @@ pub trait BlockChainClient : Sync + Send { .expect("code will return Some if given BlockId::Latest; qed") } + /// Get address code hash at given block's state. + fn code_hash(&self, address: &Address, id: BlockId) -> Option; + /// Get address balance at the given block's state. /// /// May not return None if given BlockId::Latest. diff --git a/ethcore/src/error.rs b/ethcore/src/error.rs index ae27a0940..8b0554e5a 100644 --- a/ethcore/src/error.rs +++ b/ethcore/src/error.rs @@ -83,6 +83,8 @@ pub enum TransactionError { CodeBanned, /// Invalid chain ID given. InvalidChainId, + /// Not enough permissions given by permission contract. + NotAllowed, } impl fmt::Display for TransactionError { @@ -107,6 +109,7 @@ impl fmt::Display for TransactionError { RecipientBanned => "Recipient is temporarily banned.".into(), CodeBanned => "Contract code is temporarily banned.".into(), InvalidChainId => "Transaction of this chain ID is not allowed on this chain.".into(), + NotAllowed => "Sender does not have permissions to execute this type of transction".into(), }; f.write_fmt(format_args!("Transaction error ({})", msg)) diff --git a/ethcore/src/ethereum/ethash.rs b/ethcore/src/ethereum/ethash.rs index 0b3cab6ef..b11700e09 100644 --- a/ethcore/src/ethereum/ethash.rs +++ b/ethcore/src/ethereum/ethash.rs @@ -17,7 +17,7 @@ use std::path::Path; use std::cmp; use std::collections::{BTreeMap, HashMap}; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use hash::{KECCAK_EMPTY_LIST_RLP}; use ethash::{quick_get_difficulty, slow_get_seedhash, EthashManager}; use bigint::prelude::U256; @@ -32,13 +32,15 @@ use trace::{Tracer, ExecutiveTracer, RewardType}; use header::{Header, BlockNumber}; use state::CleanupMode; use spec::CommonParams; -use transaction::UnverifiedTransaction; +use transaction::{UnverifiedTransaction, SignedTransaction}; use engines::{self, Engine}; use evm::Schedule; use ethjson; use rlp::{self, UntrustedRlp}; use vm::LastHashes; use semantic_version::SemanticVersion; +use tx_filter::{TransactionFilter}; +use client::{Client, BlockChainClient}; /// Parity tries to round block.gas_limit to multiple of this constant pub const PARITY_GAS_LIMIT_DETERMINANT: U256 = U256([37, 0, 0, 0]); @@ -144,6 +146,7 @@ pub struct Ethash { ethash_params: EthashParams, builtins: BTreeMap, pow: EthashManager, + tx_filter: Option, } impl Ethash { @@ -155,6 +158,7 @@ impl Ethash { builtins: BTreeMap, ) -> Arc { Arc::new(Ethash { + tx_filter: TransactionFilter::from_params(¶ms), params, ethash_params, builtins, @@ -440,6 +444,14 @@ impl Engine for Arc { Ok(()) } + fn verify_transaction(&self, t: UnverifiedTransaction, header: &Header) -> Result { + let signed = SignedTransaction::new(t)?; + if !self.tx_filter.as_ref().map_or(true, |filter| filter.transaction_allowed(header.parent_hash(), &signed)) { + return Err(From::from(TransactionError::NotAllowed)); + } + Ok(signed) + } + fn epoch_verifier<'a>(&self, _header: &Header, _proof: &'a [u8]) -> engines::ConstructedVerifier<'a> { engines::ConstructedVerifier::Trusted(Box::new(self.clone())) } @@ -447,6 +459,13 @@ impl Engine for Arc { fn snapshot_components(&self) -> Option> { Some(Box::new(::snapshot::PowSnapshot::new(SNAPSHOT_BLOCKS, MAX_SNAPSHOT_BLOCKS))) } + + fn register_client(&self, client: Weak) { + if let Some(ref filter) = self.tx_filter { + filter.register_client(client as Weak); + } + } + } // Try to round gas_limit a bit so that: diff --git a/ethcore/src/lib.rs b/ethcore/src/lib.rs index 382887543..2f2329d64 100644 --- a/ethcore/src/lib.rs +++ b/ethcore/src/lib.rs @@ -173,6 +173,7 @@ mod executive; mod externalities; mod blockchain; mod factory; +mod tx_filter; #[cfg(test)] mod tests; diff --git a/ethcore/src/spec/spec.rs b/ethcore/src/spec/spec.rs index 39da8cba7..727b08584 100644 --- a/ethcore/src/spec/spec.rs +++ b/ethcore/src/spec/spec.rs @@ -105,6 +105,8 @@ pub struct CommonParams { pub registrar: Address, /// Node permission managing contract address. pub node_permission_contract: Option
, + /// Transaction permission managing contract address. + pub transaction_permission_contract: Option
, } impl CommonParams { @@ -177,6 +179,7 @@ impl From for CommonParams { block_reward: p.block_reward.map_or_else(U256::zero, Into::into), registrar: p.registrar.map_or_else(Address::new, Into::into), node_permission_contract: p.node_permission_contract.map(Into::into), + transaction_permission_contract: p.transaction_permission_contract.map(Into::into), } } } diff --git a/ethcore/src/tx_filter.rs b/ethcore/src/tx_filter.rs new file mode 100644 index 000000000..f39b92451 --- /dev/null +++ b/ethcore/src/tx_filter.rs @@ -0,0 +1,233 @@ +// Copyright 2015-2017 Parity Technologies (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 . + +//! Smart contract based transaction filter. + +use std::sync::Weak; +use std::collections::HashMap; +use std::collections::hash_map::Entry; +use native_contracts::TransactAcl as Contract; +use client::{BlockChainClient, BlockId, ChainNotify}; +use util::{Address, H256, Bytes}; +use parking_lot::{Mutex, RwLock}; +use futures::{self, Future}; +use spec::CommonParams; +use transaction::{Action, SignedTransaction}; +use hash::KECCAK_EMPTY; + +const MAX_CACHE_SIZE: usize = 4096; + +mod tx_permissions { + pub const _ALL: u32 = 0xffffffff; + pub const NONE: u32 = 0x0; + pub const BASIC: u32 = 0b00000001; + pub const CALL: u32 = 0b00000010; + pub const CREATE: u32 = 0b00000100; + pub const _PRIVATE: u32 = 0b00001000; +} + +/// Connection filter that uses a contract to manage permissions. +pub struct TransactionFilter { + contract: Mutex>, + client: RwLock>>, + contract_address: Address, + permission_cache: Mutex>, +} + +impl TransactionFilter { + /// Create a new instance if address is specified in params. + pub fn from_params(params: &CommonParams) -> Option { + params.transaction_permission_contract.map(|address| + TransactionFilter { + contract: Mutex::new(None), + client: RwLock::new(None), + contract_address: address, + permission_cache: Mutex::new(HashMap::new()), + } + ) + } + + /// Clear cached permissions. + pub fn clear_cache(&self) { + self.permission_cache.lock().clear(); + } + + /// Set client reference to be used for contract call. + pub fn register_client(&self, client: Weak) { + *self.client.write() = Some(client); + } + + /// Check if transaction is allowed at given block. + pub fn transaction_allowed(&self, parent_hash: &H256, transaction: &SignedTransaction) -> bool { + self.client.read().as_ref().map_or(false, |client| { + let mut cache = self.permission_cache.lock(); let len = cache.len(); + let client = match client.upgrade() { + Some(client) => client, + _ => return false, + }; + let tx_type = match transaction.action { + Action::Create => tx_permissions::CREATE, + Action::Call(address) => if client.code_hash(&address, BlockId::Hash(*parent_hash)).map_or(false, |c| c != KECCAK_EMPTY) { + tx_permissions::CALL + } else { + tx_permissions::BASIC + } + }; + let sender = transaction.sender(); + match cache.entry((*parent_hash, sender)) { + Entry::Occupied(entry) => *entry.get() & tx_type != 0, + Entry::Vacant(entry) => { + let mut contract = self.contract.lock(); + if contract.is_none() { + *contract = Some(Contract::new(self.contract_address)); + } + + let permissions = match &*contract { + &Some(ref contract) => { + contract.allowed_tx_types( + |addr, data| futures::done(client.call_contract(BlockId::Hash(*parent_hash), addr, data)), + sender, + ).wait().unwrap_or_else(|e| { + debug!("Error callling tx permissions contract: {:?}", e); + tx_permissions::NONE + }) + } + _ => tx_permissions::NONE, + }; + + if len < MAX_CACHE_SIZE { + entry.insert(permissions); + } + trace!("Permissions required: {}, got: {}", tx_type, permissions); + permissions & tx_type != 0 + } + } + }) + } +} + +impl ChainNotify for TransactionFilter { + fn new_blocks(&self, imported: Vec, _invalid: Vec, _enacted: Vec, _retracted: Vec, _sealed: Vec, _proposed: Vec, _duration: u64) { + if !imported.is_empty() { + self.clear_cache(); + } + } +} + +#[cfg(test)] +mod test { + use std::sync::{Arc, Weak}; + use spec::Spec; + use client::{BlockChainClient, Client, ClientConfig, BlockId}; + use miner::Miner; + use util::{Address}; + use io::IoChannel; + use ethkey::{Secret, KeyPair}; + use super::TransactionFilter; + use transaction::{Transaction, Action}; + + /// Contract code: https://gist.github.com/arkpar/38a87cb50165b7e683585eec71acb05a + #[test] + fn transaction_filter() { + let spec_data = r#" + { + "name": "TestNodeFilterContract", + "engine": { + "authorityRound": { + "params": { + "stepDuration": 1, + "startStep": 2, + "validators": { + "contract": "0x0000000000000000000000000000000000000000" + } + } + } + }, + "params": { + "accountStartNonce": "0x0", + "maximumExtraDataSize": "0x20", + "minGasLimit": "0x1388", + "networkID" : "0x69", + "gasLimitBoundDivisor": "0x0400", + "transactionPermissionContract": "0x0000000000000000000000000000000000000005" + }, + "genesis": { + "seal": { + "generic": "0xc180" + }, + "difficulty": "0x20000", + "author": "0x0000000000000000000000000000000000000000", + "timestamp": "0x00", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData": "0x", + "gasLimit": "0x222222" + }, + "accounts": { + "0000000000000000000000000000000000000001": { "balance": "1", "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, + "0000000000000000000000000000000000000002": { "balance": "1", "builtin": { "name": "sha256", "pricing": { "linear": { "base": 60, "word": 12 } } } }, + "0000000000000000000000000000000000000003": { "balance": "1", "builtin": { "name": "ripemd160", "pricing": { "linear": { "base": 600, "word": 120 } } } }, + "0000000000000000000000000000000000000004": { "balance": "1", "builtin": { "name": "identity", "pricing": { "linear": { "base": 15, "word": 3 } } } }, + "0000000000000000000000000000000000000005": { + "balance": "1", + "constructor": "6060604052341561000f57600080fd5b5b6101868061001f6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063e17512211461003e575b600080fd5b341561004957600080fd5b610075600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610097565b604051808263ffffffff1663ffffffff16815260200191505060405180910390f35b6000737e5f4552091a69125d5dfcb7b8c2659029395bdf8273ffffffffffffffffffffffffffffffffffffffff1614156100d75763ffffffff9050610155565b732b5ad5c4795c026514f8317c7a215e218dccd6cf8273ffffffffffffffffffffffffffffffffffffffff1614156101155760026001179050610155565b736813eb9362372eef6200f3b1dbc3f819671cba698273ffffffffffffffffffffffffffffffffffffffff1614156101505760019050610155565b600090505b9190505600a165627a7a72305820f1f21cb978925a8a92c6e30c8c81adf598adff6d1ef941cf5ed6c0ec7ad1ae3d0029" + } + } + } + "#; + + let spec = Spec::load(::std::env::temp_dir(), spec_data.as_bytes()).unwrap(); + let client_db = Arc::new(::util::kvdb::in_memory(::db::NUM_COLUMNS.unwrap_or(0))); + + let client = Client::new( + ClientConfig::default(), + &spec, + client_db, + Arc::new(Miner::with_spec(&spec)), + IoChannel::disconnected(), + ).unwrap(); + let key1 = KeyPair::from_secret(Secret::from("0000000000000000000000000000000000000000000000000000000000000001")).unwrap(); + let key2 = KeyPair::from_secret(Secret::from("0000000000000000000000000000000000000000000000000000000000000002")).unwrap(); + let key3 = KeyPair::from_secret(Secret::from("0000000000000000000000000000000000000000000000000000000000000003")).unwrap(); + let key4 = KeyPair::from_secret(Secret::from("0000000000000000000000000000000000000000000000000000000000000004")).unwrap(); + + let filter = TransactionFilter::from_params(spec.params()).unwrap(); + filter.register_client(Arc::downgrade(&client) as Weak); + let mut basic_tx = Transaction::default(); + basic_tx.action = Action::Call(Address::from("000000000000000000000000000000000000032")); + let create_tx = Transaction::default(); + let mut call_tx = Transaction::default(); + call_tx.action = Action::Call(Address::from("0000000000000000000000000000000000000005")); + + let genesis = client.block_hash(BlockId::Latest).unwrap(); + + assert!(filter.transaction_allowed(&genesis, &basic_tx.clone().sign(key1.secret(), None))); + assert!(filter.transaction_allowed(&genesis, &create_tx.clone().sign(key1.secret(), None))); + assert!(filter.transaction_allowed(&genesis, &call_tx.clone().sign(key1.secret(), None))); + + assert!(filter.transaction_allowed(&genesis, &basic_tx.clone().sign(key2.secret(), None))); + assert!(!filter.transaction_allowed(&genesis, &create_tx.clone().sign(key2.secret(), None))); + assert!(filter.transaction_allowed(&genesis, &call_tx.clone().sign(key2.secret(), None))); + + assert!(filter.transaction_allowed(&genesis, &basic_tx.clone().sign(key3.secret(), None))); + assert!(!filter.transaction_allowed(&genesis, &create_tx.clone().sign(key3.secret(), None))); + assert!(!filter.transaction_allowed(&genesis, &call_tx.clone().sign(key3.secret(), None))); + + assert!(!filter.transaction_allowed(&genesis, &basic_tx.clone().sign(key4.secret(), None))); + assert!(!filter.transaction_allowed(&genesis, &create_tx.clone().sign(key4.secret(), None))); + assert!(!filter.transaction_allowed(&genesis, &call_tx.clone().sign(key4.secret(), None))); + } +} + diff --git a/json/src/spec/params.rs b/json/src/spec/params.rs index b90eef5ea..4561bcada 100644 --- a/json/src/spec/params.rs +++ b/json/src/spec/params.rs @@ -108,6 +108,9 @@ pub struct Params { /// Node permission contract address. #[serde(rename="nodePermissionContract")] pub node_permission_contract: Option
, + /// Transaction permission contract address. + #[serde(rename="transactionPermissionContract")] + pub transaction_permission_contract: Option
, } #[cfg(test)] diff --git a/rpc/src/v1/helpers/errors.rs b/rpc/src/v1/helpers/errors.rs index 559f3a6b9..23cbbfefc 100644 --- a/rpc/src/v1/helpers/errors.rs +++ b/rpc/src/v1/helpers/errors.rs @@ -315,6 +315,7 @@ pub fn transaction_message(error: TransactionError) -> String { SenderBanned => "Sender is banned in local queue.".into(), RecipientBanned => "Recipient is banned in local queue.".into(), CodeBanned => "Code is banned in local queue.".into(), + NotAllowed => "Transaction is not permitted.".into(), } }