From 0c517d87e7ee0cb32ac9db0a5b3b648150f9687b Mon Sep 17 00:00:00 2001 From: Daniel Porteous Date: Wed, 17 Jan 2024 18:40:28 +0100 Subject: [PATCH] [API] Add filter support for view and simulate --- api/src/transactions.rs | 17 +++++- api/src/view_function.rs | 17 +++++- config/src/config/api_config.rs | 61 +++++++++++++++++++- config/src/config/transaction_filter_type.rs | 17 +++++- 4 files changed, 107 insertions(+), 5 deletions(-) diff --git a/api/src/transactions.rs b/api/src/transactions.rs index 17156a706293a..965a58eda55fd 100644 --- a/api/src/transactions.rs +++ b/api/src/transactions.rs @@ -14,7 +14,7 @@ use crate::{ api_disabled, api_forbidden, transaction_not_found_by_hash, transaction_not_found_by_version, version_pruned, BadRequestError, BasicError, BasicErrorWith404, BasicResponse, BasicResponseStatus, BasicResult, BasicResultWith404, - InsufficientStorageError, InternalError, + ForbiddenError, InsufficientStorageError, InternalError, }, ApiTags, }; @@ -443,6 +443,21 @@ impl TransactionsApi { let ledger_info = context.get_latest_ledger_info()?; let mut signed_transaction = api.get_signed_transaction(&ledger_info, data)?; + // Confirm the simulation filter allows the transaction. We use HashValue::zero() + // here for the block ID because we don't allow filtering by block ID for the + // simulation filters. See the ConfigSanitizer for ApiConfig. + if !context.node_config.api.simulation_filter.allows( + aptos_crypto::HashValue::zero(), + ledger_info.timestamp(), + &signed_transaction, + ) { + return Err(SubmitTransactionError::forbidden_with_code( + "Transaction not allowed by simulation filter", + AptosErrorCode::InvalidInput, + &ledger_info, + )); + } + let estimated_gas_unit_price = match ( estimate_gas_unit_price.0.unwrap_or_default(), estimate_prioritized_gas_unit_price.0.unwrap_or_default(), diff --git a/api/src/view_function.rs b/api/src/view_function.rs index 7d8510e642681..ade7915215be9 100644 --- a/api/src/view_function.rs +++ b/api/src/view_function.rs @@ -8,7 +8,7 @@ use crate::{ failpoint::fail_point_poem, response::{ BadRequestError, BasicErrorWith404, BasicResponse, BasicResponseStatus, BasicResultWith404, - InternalError, + ForbiddenError, InternalError, }, ApiTags, Context, }; @@ -120,6 +120,21 @@ fn view_request( }, }; + // Reject the request if it's not allowed by the filter. + if !context.node_config.api.view_filter.allows( + view_function.module.address(), + view_function.module.name().as_str(), + view_function.function.as_str(), + ) { + return Err(BasicErrorWith404::forbidden_with_code_no_info( + format!( + "Function {}::{} is not allowed", + view_function.module, view_function.function + ), + AptosErrorCode::InvalidInput, + )); + } + let return_vals = AptosVM::execute_view_function( &state_view, view_function.module.clone(), diff --git a/config/src/config/api_config.rs b/config/src/config/api_config.rs index 16af76ecfde12..e6f950161a9bc 100644 --- a/config/src/config/api_config.rs +++ b/config/src/config/api_config.rs @@ -2,6 +2,7 @@ // Parts of the project are originally copyright © Meta Platforms, Inc. // SPDX-License-Identifier: Apache-2.0 +use super::transaction_filter_type::{Filter, Matcher}; use crate::{ config::{ config_sanitizer::ConfigSanitizer, gas_estimation_config::GasEstimationConfig, @@ -9,7 +10,7 @@ use crate::{ }, utils, }; -use aptos_types::chain_id::ChainId; +use aptos_types::{account_address::AccountAddress, chain_id::ChainId}; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; @@ -74,6 +75,10 @@ pub struct ApiConfig { pub gas_estimation: GasEstimationConfig, /// Periodically call gas estimation pub periodic_gas_estimation_ms: Option, + /// Configuration to filter simulation requests. + pub simulation_filter: Filter, + /// Configuration to filter view function requests. + pub view_filter: ViewFilter, } const DEFAULT_ADDRESS: &str = "127.0.0.1"; @@ -119,6 +124,8 @@ impl Default for ApiConfig { runtime_worker_multiplier: 2, gas_estimation: GasEstimationConfig::default(), periodic_gas_estimation_ms: Some(30_000), + simulation_filter: Filter::default(), + view_filter: ViewFilter::default(), } } } @@ -168,6 +175,16 @@ impl ConfigSanitizer for ApiConfig { )); } + // We don't support Block ID based simulation filters. + for rule in api_config.simulation_filter.rules() { + if let Matcher::BlockId(_) = rule.matcher() { + return Err(Error::ConfigSanitizerFailed( + sanitizer_name, + "Block ID based simulation filters are not supported!".into(), + )); + } + } + // Sanitize the gas estimation config GasEstimationConfig::sanitize(node_config, node_type, chain_id)?; @@ -175,6 +192,48 @@ impl ConfigSanitizer for ApiConfig { } } +// This is necessary because we can't import the EntryFunctionId type from the API types. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct ViewFunctionId { + pub address: AccountAddress, + pub module: String, + pub function_name: String, +} + +// We just accept Strings here because we can't import EntryFunctionId. We sanitize +// the values later. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ViewFilter { + /// Allowlist of functions. If a function is not found here, the API will refuse to + /// service the view / simulation request. + Allowlist(Vec), + /// Blocklist of functions. If a function is found here, the API will refuse to + /// service the view / simulation request. + Blocklist(Vec), +} + +impl Default for ViewFilter { + fn default() -> Self { + ViewFilter::Blocklist(vec![]) + } +} + +impl ViewFilter { + /// Returns true if the given function is allowed by the filter. + pub fn allows(&self, address: &AccountAddress, module: &str, function: &str) -> bool { + match self { + ViewFilter::Allowlist(ids) => ids.iter().any(|id| { + &id.address == address && id.module == module && id.function_name == function + }), + ViewFilter::Blocklist(ids) => !ids.iter().any(|id| { + &id.address == address && id.module == module && id.function_name == function + }), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/config/src/config/transaction_filter_type.rs b/config/src/config/transaction_filter_type.rs index b2b3145342099..53e2d55745efe 100644 --- a/config/src/config/transaction_filter_type.rs +++ b/config/src/config/transaction_filter_type.rs @@ -9,7 +9,7 @@ use aptos_types::{ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -enum Matcher { +pub enum Matcher { All, BlockId(HashValue), BlockTimeStampGreaterThan(u64), @@ -48,11 +48,20 @@ impl Matcher { } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -enum Rule { +pub enum Rule { Allow(Matcher), Deny(Matcher), } +impl Rule { + pub fn matcher(&self) -> &Matcher { + match self { + Rule::Allow(matcher) => matcher, + Rule::Deny(matcher) => matcher, + } + } +} + enum EvalResult { Allow, Deny, @@ -188,6 +197,10 @@ impl Filter { self } + pub fn rules(&self) -> &[Rule] { + &self.rules + } + pub fn allows(&self, block_id: HashValue, timestamp: u64, txn: &SignedTransaction) -> bool { for rule in &self.rules { // Rules are evaluated in the order and the first rule that matches is used. If no rule