From 23f60cc98e78a16efdc0f51765e98c52bcb00f8f Mon Sep 17 00:00:00 2001 From: hdbg Date: Mon, 9 Mar 2026 19:28:50 +0100 Subject: [PATCH] feat(server::evm::engine): initial wiring of all components -- we now can evaluate transactions --- server/crates/arbiter-server/src/evm/mod.rs | 194 +++++++++++++----- .../crates/arbiter-server/src/evm/policies.rs | 9 +- .../src/evm/policies/token_transfers.rs | 4 +- 3 files changed, 146 insertions(+), 61 deletions(-) diff --git a/server/crates/arbiter-server/src/evm/mod.rs b/server/crates/arbiter-server/src/evm/mod.rs index c08ad52..09dcd01 100644 --- a/server/crates/arbiter-server/src/evm/mod.rs +++ b/server/crates/arbiter-server/src/evm/mod.rs @@ -1,26 +1,61 @@ -pub mod safe_signer; pub mod abi; +pub mod safe_signer; -use alloy::{ - consensus::TxEip1559, - primitives::TxKind, -}; -use diesel::insert_into; +use alloy::{consensus::TxEip1559, primitives::TxKind, signers::Signature}; +use chrono::Utc; +use diesel::{QueryResult, insert_into}; use diesel_async::{AsyncConnection, RunQueryDsl}; use crate::{ db::{ self, - models::{EvmBasicGrant, NewEvmBasicGrant, SqliteTimestamp}, schema, + models::{ + EvmBasicGrant, EvmTransactionLog, NewEvmBasicGrant, NewEvmTransactionLog, + SqliteTimestamp, + }, + schema::{self, evm_transaction_log}, }, evm::policies::{ - EvalContext, FullGrant, Policy, SpecificMeaning, ether_transfer::EtherTransfer + EvalContext, EvalViolation, FullGrant, Policy, SpecificMeaning, + ether_transfer::EtherTransfer, token_transfers::TokenTransfer, }, }; pub mod policies; mod utils; +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +pub enum VetError { + #[error("Database connection pool error")] + #[diagnostic(code(arbiter_server::evm::vet_error::database_error))] + Pool(#[from] db::PoolError), + #[error("Database returned error")] + #[diagnostic(code(arbiter_server::evm::vet_error::database_error))] + Database(#[from] diesel::result::Error), + #[error("Transaction violates policy: {0:?}")] + #[diagnostic(code(arbiter_server::evm::vet_error::policy_violation))] + Violations(Vec), + #[error("No matching grant found")] + #[diagnostic(code(arbiter_server::evm::vet_error::no_matching_grant))] + NoMatchingGrant, + #[error("Contract creation transactions are not supported")] + #[diagnostic(code(arbiter_server::evm::vet_error::contract_creation_unsupported))] + ContractCreationNotSupported, + #[error("Engine can't classify this transaction")] + #[diagnostic(code(arbiter_server::evm::vet_error::unsupported))] + UnsupportedTransactionType, +} + +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +pub enum SignError { + #[error("Database connection pool error")] + #[diagnostic(code(arbiter_server::evm::database_error))] + Pool(#[from] db::PoolError), + #[error("Database returned error")] + #[diagnostic(code(arbiter_server::evm::database_error))] + Database(#[from] diesel::result::Error), +} + #[derive(Debug, thiserror::Error, miette::Diagnostic)] pub enum AnalyzeError { #[error("Engine doesn't support granting permissions for contract creation")] @@ -48,6 +83,50 @@ pub struct Engine { db: db::DatabasePool, } +impl Engine { + async fn vet_transaction( + &self, + context: EvalContext, + meaning: &P::Meaning, + dry_run: bool, + ) -> Result<(), VetError> { + let mut conn = self.db.get().await?; + + let grant = P::try_find_grant(&context, &mut conn) + .await? + .ok_or(VetError::NoMatchingGrant)?; + + let violations = P::evaluate(&context, meaning, &grant, &mut conn).await?; + if !violations.is_empty() { + return Err(VetError::Violations(violations)); + } else if !dry_run { + conn.transaction(|conn| { + Box::pin(async move { + let log_id: i32 = insert_into(evm_transaction_log::table) + .values(&NewEvmTransactionLog { + grant_id: grant.shared_grant_id, + client_id: context.client_id, + wallet_id: context.wallet_id, + chain_id: context.chain as i32, + eth_value: utils::u256_to_bytes(context.value).to_vec(), + signed_at: Utc::now().into(), + }) + .returning(evm_transaction_log::id) + .get_result(conn) + .await?; + + P::record_transaction(&context, meaning, log_id, &grant, conn).await?; + + QueryResult::Ok(()) + }) + }) + .await?; + } + + Ok(()) + } +} + impl Engine { pub fn new(db: db::DatabasePool) -> Self { Self { db } @@ -60,60 +139,60 @@ impl Engine { ) -> Result { let mut conn = self.db.get().await?; - let id = conn.transaction(|conn| { - Box::pin(async move { - use schema::evm_basic_grant; + let id = conn + .transaction(|conn| { + Box::pin(async move { + use schema::evm_basic_grant; - let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table) - .values(&NewEvmBasicGrant { - wallet_id: full_grant.basic.wallet_id, - chain_id: full_grant.basic.chain as i32, - client_id: client_id, - valid_from: full_grant.basic.valid_from.map(SqliteTimestamp), - valid_until: full_grant.basic.valid_until.map(SqliteTimestamp), - max_gas_fee_per_gas: full_grant - .basic - .max_gas_fee_per_gas - .map(|fee| utils::u256_to_bytes(fee).to_vec()), - max_priority_fee_per_gas: full_grant - .basic - .max_priority_fee_per_gas - .map(|fee| utils::u256_to_bytes(fee).to_vec()), - rate_limit_count: full_grant - .basic - .rate_limit - .as_ref() - .map(|rl| rl.count as i32), - rate_limit_window_secs: full_grant - .basic - .rate_limit - .as_ref() - .map(|rl| rl.window.num_seconds() as i32), - revoked_at: None, - }) - .returning(evm_basic_grant::all_columns) - .get_result(conn) - .await?; + let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table) + .values(&NewEvmBasicGrant { + wallet_id: full_grant.basic.wallet_id, + chain_id: full_grant.basic.chain as i32, + client_id: client_id, + valid_from: full_grant.basic.valid_from.map(SqliteTimestamp), + valid_until: full_grant.basic.valid_until.map(SqliteTimestamp), + max_gas_fee_per_gas: full_grant + .basic + .max_gas_fee_per_gas + .map(|fee| utils::u256_to_bytes(fee).to_vec()), + max_priority_fee_per_gas: full_grant + .basic + .max_priority_fee_per_gas + .map(|fee| utils::u256_to_bytes(fee).to_vec()), + rate_limit_count: full_grant + .basic + .rate_limit + .as_ref() + .map(|rl| rl.count as i32), + rate_limit_window_secs: full_grant + .basic + .rate_limit + .as_ref() + .map(|rl| rl.window.num_seconds() as i32), + revoked_at: None, + }) + .returning(evm_basic_grant::all_columns) + .get_result(conn) + .await?; - P::create_grant(&basic_grant, &full_grant.specific, conn).await + P::create_grant(&basic_grant, &full_grant.specific, conn).await + }) }) - }) - .await?; + .await?; Ok(id) } - async fn perform_transaction(&self, _context: EvalContext, _meaning: &P::Meaning) { - } - - pub async fn analyze_transaction( + pub async fn evaluate_transaction( &self, wallet_id: i32, client_id: i32, transaction: TxEip1559, - ) -> Result { + // don't log + dry_run: bool, + ) -> Result<(), VetError> { let TxKind::Call(to) = transaction.to else { - return Err(AnalyzeError::ContractCreationNotSupported); + return Err(VetError::ContractCreationNotSupported); }; let context = policies::EvalContext { wallet_id, @@ -124,10 +203,17 @@ impl Engine { calldata: transaction.input.clone(), }; - if let Some(meaning) = EtherTransfer::analyze(&context) { - return Ok(SpecificMeaning::EtherTransfer(meaning)); + return self + .vet_transaction::(context, &meaning, dry_run) + .await; } - Err(AnalyzeError::UnsupportedTransactionType) + if let Some(meaning) = TokenTransfer::analyze(&context) { + return self + .vet_transaction::(context, &meaning, dry_run) + .await; + } + + Err(VetError::UnsupportedTransactionType) } -} \ No newline at end of file +} diff --git a/server/crates/arbiter-server/src/evm/policies.rs b/server/crates/arbiter-server/src/evm/policies.rs index aa6e63d..fcb875e 100644 --- a/server/crates/arbiter-server/src/evm/policies.rs +++ b/server/crates/arbiter-server/src/evm/policies.rs @@ -3,8 +3,7 @@ use std::fmt::Display; use alloy::primitives::{Address, Bytes, ChainId, U256}; use chrono::{DateTime, Duration, Utc}; use diesel::{ - ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, - sqlite::Sqlite, + ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite, }; use diesel_async::{AsyncConnection, RunQueryDsl}; use miette::Diagnostic; @@ -70,8 +69,8 @@ pub struct Grant { } pub trait Policy: Sized { - type Settings: Send + 'static + Into; - type Meaning: Display + Send + 'static + Into; + type Settings: Send + Sync + 'static + Into; + type Meaning: Display + Send + Sync + 'static + Into; fn analyze(context: &EvalContext) -> Option; @@ -106,7 +105,7 @@ pub trait Policy: Sized { log_id: i32, grant: &Grant, conn: &mut impl AsyncConnection, - ) -> impl Future>; + ) -> impl Future> + Send; } pub enum ReceiverTarget { diff --git a/server/crates/arbiter-server/src/evm/policies/token_transfers.rs b/server/crates/arbiter-server/src/evm/policies/token_transfers.rs index 6a8d092..f21fc0c 100644 --- a/server/crates/arbiter-server/src/evm/policies/token_transfers.rs +++ b/server/crates/arbiter-server/src/evm/policies/token_transfers.rs @@ -114,8 +114,8 @@ async fn check_volume_rate_limits( Ok(violations) } -pub struct TokenTransferPolicy; -impl Policy for TokenTransferPolicy { +pub struct TokenTransfer; +impl Policy for TokenTransfer { type Settings = Settings; type Meaning = Meaning;