diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 402dab961..ce03320fe 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1664,6 +1664,26 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +[[package]] +name = "lz4" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9e2dd86df36ce760a60f6ff6ad526f7ba1f14ba0356f8254fb6905e6494df1" +dependencies = [ + "libc", + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "macros" version = "0.1.0" @@ -3146,18 +3166,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", @@ -3597,6 +3617,19 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "transaction-filter" +version = "0.1.0" +dependencies = [ + "anyhow", + "aptos-protos", + "lz4", + "prost 0.12.3", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "try-lock" version = "0.2.4" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 3f3d86076..af64c08e7 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" -members = ["indexer-metrics", "moving-average", "processor", "server-framework"] +members = ["indexer-metrics", "moving-average", "processor", "server-framework", "transaction-filter"] [workspace.package] authors = ["Aptos Labs "] @@ -86,6 +86,7 @@ sha2 = "0.9.3" sha3 = "0.9.1" strum = { version = "0.24.1", features = ["derive"] } tempfile = "3.3.0" +thiserror = "1.0.61" toml = "0.7.4" tracing-subscriber = { version = "0.3.17", features = ["json", "env-filter"] } tokio = { version = "1.35.1", features = ["full"] } diff --git a/rust/transaction-filter/Cargo.toml b/rust/transaction-filter/Cargo.toml new file mode 100644 index 000000000..7fe85ab00 --- /dev/null +++ b/rust/transaction-filter/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "transaction-filter" +version = "0.1.0" + +# Workspace inherited keys +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +publish = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { workspace = true } +aptos-protos = { workspace = true } + +prost = { workspace = true } + +serde = { workspace = true } +serde_json = { workspace = true } + +thiserror = { workspace = true } + +[dev-dependencies] +# we only decompress the fixture protos in tests +lz4 = "1.24.0" + diff --git a/rust/transaction-filter/fixtures/compressed_files_lz4_00008bc1d5adcf862d3967c1410001fb_705101000.pb.lz4 b/rust/transaction-filter/fixtures/compressed_files_lz4_00008bc1d5adcf862d3967c1410001fb_705101000.pb.lz4 new file mode 100644 index 000000000..cee326c26 Binary files /dev/null and b/rust/transaction-filter/fixtures/compressed_files_lz4_00008bc1d5adcf862d3967c1410001fb_705101000.pb.lz4 differ diff --git a/rust/transaction-filter/fixtures/compressed_files_lz4_0013c194ec4fdbfb8db7306170aac083_445907000.pb.lz4 b/rust/transaction-filter/fixtures/compressed_files_lz4_0013c194ec4fdbfb8db7306170aac083_445907000.pb.lz4 new file mode 100644 index 000000000..ab76ef837 Binary files /dev/null and b/rust/transaction-filter/fixtures/compressed_files_lz4_0013c194ec4fdbfb8db7306170aac083_445907000.pb.lz4 differ diff --git a/rust/transaction-filter/fixtures/compressed_files_lz4_f3d880d9700c70d71fefe71aa9218aa9_301616000.pb.lz4 b/rust/transaction-filter/fixtures/compressed_files_lz4_f3d880d9700c70d71fefe71aa9218aa9_301616000.pb.lz4 new file mode 100644 index 000000000..512e84e77 Binary files /dev/null and b/rust/transaction-filter/fixtures/compressed_files_lz4_f3d880d9700c70d71fefe71aa9218aa9_301616000.pb.lz4 differ diff --git a/rust/transaction-filter/src/filters/event_filter.rs b/rust/transaction-filter/src/filters/event_filter.rs new file mode 100644 index 000000000..1935467b4 --- /dev/null +++ b/rust/transaction-filter/src/filters/event_filter.rs @@ -0,0 +1,39 @@ +use crate::{filters::MoveStructTagFilter, traits::Filterable}; +use anyhow::Error; +use aptos_protos::transaction::v1::{move_type::Content, Event}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct EventFilter { + // Only for events that have a struct as their generic + #[serde(skip_serializing_if = "Option::is_none")] + pub struct_type: Option, +} + +impl Filterable for EventFilter { + fn is_valid(&self) -> Result<(), Error> { + if self.struct_type.is_none() { + return Err(Error::msg("At least one of struct_type must be set")); + }; + + self.struct_type.is_valid()?; + Ok(()) + } + + fn is_allowed(&self, item: &Event) -> bool { + if let Some(struct_type_filter) = &self.struct_type { + if let Some(Content::Struct(struct_tag)) = + &item.r#type.as_ref().and_then(|t| t.content.as_ref()) + { + if !struct_type_filter.is_allowed(struct_tag) { + return false; + } + } else { + return false; + } + } + + true + } +} diff --git a/rust/transaction-filter/src/filters/mod.rs b/rust/transaction-filter/src/filters/mod.rs new file mode 100644 index 000000000..d7c76b9fb --- /dev/null +++ b/rust/transaction-filter/src/filters/mod.rs @@ -0,0 +1,12 @@ +pub mod event_filter; +pub mod move_module; +pub mod transaction_root; +pub mod user_transaction_request; +pub mod write_set_change_filter; + +// Re-export for easier use +pub use event_filter::EventFilter; +pub use move_module::{MoveModuleFilter, MoveStructTagFilter}; +pub use transaction_root::TransactionRootFilter; +pub use user_transaction_request::{UserTransactionPayloadFilter, UserTransactionRequestFilter}; +pub use write_set_change_filter::WriteSetChangeFilter; diff --git a/rust/transaction-filter/src/filters/move_module.rs b/rust/transaction-filter/src/filters/move_module.rs new file mode 100644 index 000000000..eafa72fe6 --- /dev/null +++ b/rust/transaction-filter/src/filters/move_module.rs @@ -0,0 +1,54 @@ +use crate::traits::Filterable; +use anyhow::{anyhow, Error}; +use aptos_protos::transaction::v1::{MoveModuleId, MoveStructTag}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MoveModuleFilter { + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl Filterable for MoveModuleFilter { + fn is_valid(&self) -> Result<(), Error> { + if self.address.is_none() && self.name.is_none() { + return Err(anyhow!("At least one of address or name must be set")); + }; + Ok(()) + } + + fn is_allowed(&self, module_id: &MoveModuleId) -> bool { + self.address.is_allowed(&module_id.address) && self.name.is_allowed(&module_id.name) + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MoveStructTagFilter { + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub module: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl Filterable for MoveStructTagFilter { + fn is_valid(&self) -> Result<(), Error> { + if self.address.is_none() && self.module.is_none() && self.name.is_none() { + return Err(anyhow!( + "At least one of address, module or name must be set" + )); + }; + Ok(()) + } + + fn is_allowed(&self, struct_tag: &MoveStructTag) -> bool { + self.address.is_allowed(&struct_tag.address) + && self.module.is_allowed(&struct_tag.module) + && self.name.is_allowed(&struct_tag.name) + } +} diff --git a/rust/transaction-filter/src/filters/transaction_root.rs b/rust/transaction-filter/src/filters/transaction_root.rs new file mode 100644 index 000000000..d611c01b0 --- /dev/null +++ b/rust/transaction-filter/src/filters/transaction_root.rs @@ -0,0 +1,43 @@ +use crate::traits::Filterable; +use anyhow::Error; +use aptos_protos::transaction::v1::{transaction::TransactionType, Transaction}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct TransactionRootFilter { + #[serde(skip_serializing_if = "Option::is_none")] + pub success: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub txn_type: Option, +} + +impl Filterable for TransactionRootFilter { + fn is_valid(&self) -> Result<(), Error> { + if self.success.is_none() && self.txn_type.is_none() { + return Err(Error::msg( + "At least one of success or txn_types must be set", + )); + }; + Ok(()) + } + + fn is_allowed(&self, item: &Transaction) -> bool { + if !self + .success + .is_allowed_opt(&item.info.as_ref().map(|i| i.success)) + { + return false; + } + + if let Some(txn_type) = &self.txn_type { + if txn_type + != &TransactionType::try_from(item.r#type).expect("Invalid transaction type") + { + return false; + } + } + + true + } +} diff --git a/rust/transaction-filter/src/filters/user_transaction_request.rs b/rust/transaction-filter/src/filters/user_transaction_request.rs new file mode 100644 index 000000000..aa597f257 --- /dev/null +++ b/rust/transaction-filter/src/filters/user_transaction_request.rs @@ -0,0 +1,139 @@ +use crate::traits::Filterable; +use anyhow::{anyhow, Error}; +use aptos_protos::transaction::v1::{ + multisig_transaction_payload, transaction_payload, EntryFunctionId, EntryFunctionPayload, + TransactionPayload, UserTransactionRequest, +}; +use serde::{Deserialize, Serialize}; + +/// We use this for UserTransactions. +/// We support UserPayload and MultisigPayload +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct UserTransactionRequestFilter { + #[serde(skip_serializing_if = "Option::is_none")] + pub sender: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payload: Option, +} + +impl Filterable for UserTransactionRequestFilter { + fn is_valid(&self) -> Result<(), Error> { + if self.sender.is_none() && self.payload.is_none() { + return Err(Error::msg("At least one of sender or payload must be set")); + }; + self.payload.is_valid()?; + Ok(()) + } + + fn is_allowed(&self, item: &UserTransactionRequest) -> bool { + if let Some(sender_filter) = &self.sender { + if &item.sender != sender_filter { + return false; + } + } + + if let Some(payload_filter) = &self.payload { + // Get the entry_function_payload from both UserPayload and MultisigPayload + let entry_function_payload = item + .payload + .as_ref() + .and_then(get_entry_function_payload_from_transaction_payload); + if let Some(payload) = entry_function_payload { + // Here we have an actual EntryFunctionPayload + if !payload_filter.is_allowed(payload) { + return false; + } + } + } + + true + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct EntryFunctionFilter { + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub method: Option, +} + +impl Filterable for EntryFunctionFilter { + fn is_valid(&self) -> Result<(), Error> { + if self.address.is_none() && self.name.is_none() && self.method.is_none() { + return Err(anyhow!( + "At least one of address, name or method must be set" + )); + }; + Ok(()) + } + + fn is_allowed(&self, module_id: &EntryFunctionId) -> bool { + if !self.name.is_allowed(&module_id.name) { + return false; + } + + if self.address.is_some() || self.method.is_some() { + if let Some(module) = &module_id.module.as_ref() { + if !(self.address.is_allowed(&module.address) + && self.method.is_allowed(&module.name)) + { + return false; + } + } else { + return false; + } + } + + true + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct UserTransactionPayloadFilter { + #[serde(skip_serializing_if = "Option::is_none")] + pub function: Option, +} + +impl Filterable for UserTransactionPayloadFilter { + fn is_valid(&self) -> Result<(), Error> { + if self.function.is_none() { + return Err(Error::msg("At least one of function must be set")); + }; + self.function.is_valid()?; + Ok(()) + } + + fn is_allowed(&self, payload: &EntryFunctionPayload) -> bool { + self.function.is_allowed_opt(&payload.function) + } +} + +/// Get the entry_function_payload from both UserPayload and MultisigPayload +fn get_entry_function_payload_from_transaction_payload( + payload: &TransactionPayload, +) -> Option<&EntryFunctionPayload> { + let z = if let Some(payload) = &payload.payload { + match payload { + transaction_payload::Payload::EntryFunctionPayload(ef_payload) => Some(ef_payload), + transaction_payload::Payload::MultisigPayload(ms_payload) => ms_payload + .transaction_payload + .as_ref() + .and_then(|tp| tp.payload.as_ref()) + .map(|payload| match payload { + multisig_transaction_payload::Payload::EntryFunctionPayload(ef_payload) => { + ef_payload + }, + }), + _ => None, + } + } else { + None + }; + z +} diff --git a/rust/transaction-filter/src/filters/write_set_change_filter.rs b/rust/transaction-filter/src/filters/write_set_change_filter.rs new file mode 100644 index 000000000..d78ec662e --- /dev/null +++ b/rust/transaction-filter/src/filters/write_set_change_filter.rs @@ -0,0 +1,305 @@ +use crate::{filters::MoveStructTagFilter, traits::Filterable}; +use anyhow::Error; +use aptos_protos::transaction::v1::{ + write_set_change::{Change, Type as ChangeType}, + DeleteModule, DeleteResource, DeleteTableItem, WriteModule, WriteResource, WriteSetChange, + WriteTableItem, +}; +use serde::{Deserialize, Serialize}; + +/// This is a wrapper around ChangeItemFilter, which differs because: +/// While `ChangeItemFilter` will return false if the Event does not match the filter, +/// `ChangeItemFilter` will return true- i.e `WriteSetChangeFilter` *only* tries to match if the +/// change type matches its internal change type +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WriteSetChangeFilter { + pub change_type: Option, + // TODO: handle actual changes!!! + pub change: Option, +} + +impl Filterable for WriteSetChangeFilter { + fn is_valid(&self) -> Result<(), Error> { + if self.change_type.is_none() && self.change.is_none() { + return Err(Error::msg( + "At least one of change_type or change must be set", + )); + }; + self.change.is_valid()?; + Ok(()) + } + + fn is_allowed(&self, item: &WriteSetChange) -> bool { + if let Some(change_type) = &self.change_type { + if (*change_type as i32) != item.r#type { + return false; + } + } + + if let Some(change_filter) = &self.change { + if let Some(change) = item.change.as_ref() { + match change { + Change::DeleteModule(dm) => { + if let ChangeItemFilter::ModuleChange(mcf) = change_filter { + if !mcf.is_allowed(&ModuleChange::DeleteModule(dm)) { + return false; + } + } + }, + Change::WriteModule(wm) => { + if let ChangeItemFilter::ModuleChange(mcf) = change_filter { + if !mcf.is_allowed(&ModuleChange::WriteModule(wm)) { + return false; + } + } + }, + Change::DeleteResource(dr) => { + if let ChangeItemFilter::ResourceChange(rcf) = change_filter { + if !rcf.is_allowed(&ResourceChange::DeleteResource(dr)) { + return false; + } + } + }, + Change::WriteResource(wr) => { + if let ChangeItemFilter::ResourceChange(rcf) = change_filter { + if !rcf.is_allowed(&ResourceChange::WriteResource(wr)) { + return false; + } + } + }, + Change::DeleteTableItem(dti) => { + if let ChangeItemFilter::TableChange(tcf) = change_filter { + if !tcf.is_allowed(&TableChange::DeleteTableItem(dti)) { + return false; + } + } + }, + Change::WriteTableItem(wti) => { + if let ChangeItemFilter::TableChange(tcf) = change_filter { + if !tcf.is_allowed(&TableChange::WriteTableItem(wti)) { + return false; + } + } + }, + } + } else { + return false; + } + } + + true + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +#[serde(tag = "type")] +pub enum ChangeItemFilter { + ResourceChange(ResourceChangeFilter), + ModuleChange(ModuleChangeFilter), + TableChange(TableChangeFilter), +} + +impl Filterable for ChangeItemFilter { + fn is_valid(&self) -> Result<(), Error> { + match self { + ChangeItemFilter::ResourceChange(rcf) => rcf.is_valid(), + ChangeItemFilter::ModuleChange(mcf) => mcf.is_valid(), + ChangeItemFilter::TableChange(tcf) => tcf.is_valid(), + } + } + + fn is_allowed(&self, item: &Change) -> bool { + match item { + Change::DeleteModule(dm) => { + if let ChangeItemFilter::ModuleChange(mcf) = self { + return mcf.is_allowed(&ModuleChange::DeleteModule(dm)); + } + false + }, + Change::WriteModule(wm) => { + if let ChangeItemFilter::ModuleChange(mcf) = self { + return mcf.is_allowed(&ModuleChange::WriteModule(wm)); + } + false + }, + Change::DeleteResource(dr) => { + if let ChangeItemFilter::ResourceChange(rcf) = self { + return rcf.is_allowed(&ResourceChange::DeleteResource(dr)); + } + false + }, + Change::WriteResource(wr) => { + if let ChangeItemFilter::ResourceChange(rcf) = self { + return rcf.is_allowed(&ResourceChange::WriteResource(wr)); + } + false + }, + Change::DeleteTableItem(dti) => { + if let ChangeItemFilter::TableChange(tcf) = self { + return tcf.is_allowed(&TableChange::DeleteTableItem(dti)); + } + false + }, + Change::WriteTableItem(wti) => { + if let ChangeItemFilter::TableChange(tcf) = self { + return tcf.is_allowed(&TableChange::WriteTableItem(wti)); + } + false + }, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ResourceChangeFilter { + // todo: handle `generic_type_params` as well + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, +} + +pub enum ResourceChange<'a> { + DeleteResource(&'a DeleteResource), + WriteResource(&'a WriteResource), +} + +impl Filterable> for ResourceChangeFilter { + fn is_valid(&self) -> Result<(), Error> { + if self.resource_type.is_none() && self.address.is_none() { + return Err(Error::msg( + "At least one of resource_type, address must be set", + )); + }; + self.resource_type.is_valid()?; + Ok(()) + } + + fn is_allowed(&self, item: &ResourceChange) -> bool { + match &item { + ResourceChange::DeleteResource(dr) => { + if let Some(address) = &self.address { + if address != &dr.address { + return false; + } + } + if let Some(resource_type) = &self.resource_type { + if !resource_type.is_allowed_opt(&dr.r#type) { + return false; + } + } + }, + ResourceChange::WriteResource(wr) => { + if let Some(address) = &self.address { + if address != &wr.address { + return false; + } + } + if let Some(resource_type) = &self.resource_type { + if !resource_type.is_allowed_opt(&wr.r#type) { + return false; + } + } + }, + } + true + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ModuleChangeFilter { + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, +} +pub enum ModuleChange<'a> { + DeleteModule(&'a DeleteModule), + WriteModule(&'a WriteModule), +} + +impl Filterable> for ModuleChangeFilter { + fn is_valid(&self) -> Result<(), Error> { + if self.address.is_none() { + return Err(Error::msg("At least one of address must be set")); + }; + Ok(()) + } + + fn is_allowed(&self, item: &ModuleChange) -> bool { + if let Some(address) = &self.address { + return match &item { + ModuleChange::DeleteModule(dm) => address == &dm.address, + ModuleChange::WriteModule(wm) => address == &wm.address, + }; + } + true + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct TableChangeFilter { + pub handle: Option, + pub key: Option, + pub key_type_str: Option, +} + +pub enum TableChange<'a> { + DeleteTableItem(&'a DeleteTableItem), + WriteTableItem(&'a WriteTableItem), +} +impl Filterable> for TableChangeFilter { + fn is_valid(&self) -> Result<(), Error> { + if self.handle.is_none() && self.key.is_none() && self.key_type_str.is_none() { + return Err(Error::msg( + "At least one of handle, key, or key_type must be set", + )); + }; + Ok(()) + } + + fn is_allowed(&self, item: &TableChange) -> bool { + match &item { + TableChange::DeleteTableItem(dti) => { + if let Some(handle) = &self.handle { + return handle == &dti.handle; + } + if let Some(key_type) = &self.key_type_str { + if !dti + .data + .as_ref() + .map_or(false, |dtd| key_type == &dtd.key_type) + { + return false; + } + } + if let Some(key) = &self.key { + if !dti.data.as_ref().map_or(false, |dtd| key == &dtd.key) { + return false; + } + } + }, + TableChange::WriteTableItem(wti) => { + if let Some(handle) = &self.handle { + if handle != &wti.handle { + return false; + } + } + if let Some(key_type) = &self.key_type_str { + if !wti + .data + .as_ref() + .map_or(false, |wtd| key_type == &wtd.key_type) + { + return false; + } + } + self.key.is_allowed(&wti.key); + }, + } + true + } +} diff --git a/rust/transaction-filter/src/lib.rs b/rust/transaction-filter/src/lib.rs new file mode 100644 index 000000000..949b9e94d --- /dev/null +++ b/rust/transaction-filter/src/lib.rs @@ -0,0 +1,32 @@ +pub mod filters; +pub mod traits; + +/** +The goal of transaction filtering is to be able to save resources downstream of wherever filtering is used. +For this to be true, the filtering itself must be fast, and so we do a few things: + 1. We avoid clones, copies, etc as much as possible + 2. We do a single pass over the transaction data + +There are four different parts of a transaction that are queryable: + 1. The "root" level. This includes: + - Transaction type + - Success + 2. Arbitrary Transaction-type-specific filters. We currently only support the "user" transaction type. + - Sender + - Payload: we only support the entry function payload + - Entry function (address, module, name) + - Entry function ID string + 3. Events. Each event has: + - Key + - Type + 4. WriteSet Changes. Each change may have: + - Type + - Address +**/ + +#[cfg(test)] +mod tests { + + #[test] + fn it_works() {} +} diff --git a/rust/transaction-filter/src/traits.rs b/rust/transaction-filter/src/traits.rs new file mode 100644 index 000000000..33f91ab71 --- /dev/null +++ b/rust/transaction-filter/src/traits.rs @@ -0,0 +1,94 @@ +use anyhow::Error; + +/// Simple trait to allow for filtering of items of type T +pub trait Filterable { + /// Whether this filter is correctly configured/initialized + /// Any call to `is_valid` is responsible for recursively checking the validity of any nested filters + fn is_valid(&self) -> Result<(), Error>; + + fn is_allowed(&self, item: &T) -> bool; + + fn is_allowed_vec(&self, items: &[T]) -> bool { + items.iter().all(|item| self.is_allowed(item)) + } + + fn is_allowed_opt(&self, item: &Option) -> bool { + match item { + Some(item) => self.is_allowed(item), + None => false, + } + } + + fn is_allowed_opt_vec(&self, items: &Option<&Vec>) -> bool { + match items { + Some(items) => self.is_allowed_vec(items), + None => false, + } + } +} + +/// This allows for Option to always return true: i.e if the filter is None, then all items are allowed. +impl Filterable for Option +where + F: Filterable, +{ + fn is_valid(&self) -> Result<(), Error> { + match self { + Some(filter) => filter.is_valid(), + None => Ok(()), + } + } + + fn is_allowed(&self, item: &T) -> bool { + match self { + Some(filter) => filter.is_allowed(item), + None => true, + } + } + + fn is_allowed_opt(&self, item: &Option) -> bool { + match self { + Some(filter) => filter.is_allowed_opt(item), + None => true, + } + } +} + +impl Filterable for Option { + fn is_valid(&self) -> Result<(), Error> { + Ok(()) + } + + fn is_allowed(&self, item: &String) -> bool { + match self { + Some(filter) => filter == item, + None => true, + } + } +} + +impl Filterable for Option { + fn is_valid(&self) -> Result<(), Error> { + Ok(()) + } + + fn is_allowed(&self, item: &i32) -> bool { + match self { + Some(filter) => filter == item, + None => true, + } + } +} + +impl Filterable for Option { + fn is_valid(&self) -> Result<(), Error> { + Ok(()) + } + + fn is_allowed(&self, item: &bool) -> bool { + match self { + Some(filter) => filter == item, + None => true, + } + } +}