Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API] Add filter support for view and simulate #11666

Merged
merged 1 commit into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 16 additions & 1 deletion api/src/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably, it could possibly be a different code, but that's fine. InvalidInput always requires user intervention.

&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(),
Expand Down
17 changes: 16 additions & 1 deletion api/src/view_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
failpoint::fail_point_poem,
response::{
BadRequestError, BasicErrorWith404, BasicResponse, BasicResponseStatus, BasicResultWith404,
InternalError,
ForbiddenError, InternalError,
},
ApiTags, Context,
};
Expand Down Expand Up @@ -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(),
Expand Down
61 changes: 60 additions & 1 deletion config/src/config/api_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
// 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,
node_config_loader::NodeType, Error, NodeConfig,
},
utils,
};
use aptos_types::chain_id::ChainId;
use aptos_types::{account_address::AccountAddress, chain_id::ChainId};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;

Expand Down Expand Up @@ -74,6 +75,10 @@ pub struct ApiConfig {
pub gas_estimation: GasEstimationConfig,
/// Periodically call gas estimation
pub periodic_gas_estimation_ms: Option<u64>,
/// 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";
Expand Down Expand Up @@ -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(),
}
}
}
Expand Down Expand Up @@ -168,13 +175,65 @@ 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)?;

Ok(())
}
}

// 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<ViewFunctionId>),
/// Blocklist of functions. If a function is found here, the API will refuse to
/// service the view / simulation request.
Blocklist(Vec<ViewFunctionId>),
}

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::*;
Expand Down
17 changes: 15 additions & 2 deletions config/src/config/transaction_filter_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading