feat(server::evm::engine): initial wiring of all components -- we now can evaluate transactions

This commit is contained in:
hdbg
2026-03-09 19:28:50 +01:00
parent 0c85e1f167
commit 23f60cc98e
3 changed files with 146 additions and 61 deletions

View File

@@ -1,26 +1,61 @@
pub mod safe_signer;
pub mod abi; pub mod abi;
pub mod safe_signer;
use alloy::{ use alloy::{consensus::TxEip1559, primitives::TxKind, signers::Signature};
consensus::TxEip1559, use chrono::Utc;
primitives::TxKind, use diesel::{QueryResult, insert_into};
};
use diesel::insert_into;
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
use crate::{ use crate::{
db::{ db::{
self, self,
models::{EvmBasicGrant, NewEvmBasicGrant, SqliteTimestamp}, schema, models::{
EvmBasicGrant, EvmTransactionLog, NewEvmBasicGrant, NewEvmTransactionLog,
SqliteTimestamp,
},
schema::{self, evm_transaction_log},
}, },
evm::policies::{ evm::policies::{
EvalContext, FullGrant, Policy, SpecificMeaning, ether_transfer::EtherTransfer EvalContext, EvalViolation, FullGrant, Policy, SpecificMeaning,
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
}, },
}; };
pub mod policies; pub mod policies;
mod utils; 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<EvalViolation>),
#[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)] #[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum AnalyzeError { pub enum AnalyzeError {
#[error("Engine doesn't support granting permissions for contract creation")] #[error("Engine doesn't support granting permissions for contract creation")]
@@ -48,6 +83,50 @@ pub struct Engine {
db: db::DatabasePool, db: db::DatabasePool,
} }
impl Engine {
async fn vet_transaction<P: Policy>(
&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 { impl Engine {
pub fn new(db: db::DatabasePool) -> Self { pub fn new(db: db::DatabasePool) -> Self {
Self { db } Self { db }
@@ -60,60 +139,60 @@ impl Engine {
) -> Result<i32, CreationError> { ) -> Result<i32, CreationError> {
let mut conn = self.db.get().await?; let mut conn = self.db.get().await?;
let id = conn.transaction(|conn| { let id = conn
Box::pin(async move { .transaction(|conn| {
use schema::evm_basic_grant; Box::pin(async move {
use schema::evm_basic_grant;
let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table) let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table)
.values(&NewEvmBasicGrant { .values(&NewEvmBasicGrant {
wallet_id: full_grant.basic.wallet_id, wallet_id: full_grant.basic.wallet_id,
chain_id: full_grant.basic.chain as i32, chain_id: full_grant.basic.chain as i32,
client_id: client_id, client_id: client_id,
valid_from: full_grant.basic.valid_from.map(SqliteTimestamp), valid_from: full_grant.basic.valid_from.map(SqliteTimestamp),
valid_until: full_grant.basic.valid_until.map(SqliteTimestamp), valid_until: full_grant.basic.valid_until.map(SqliteTimestamp),
max_gas_fee_per_gas: full_grant max_gas_fee_per_gas: full_grant
.basic .basic
.max_gas_fee_per_gas .max_gas_fee_per_gas
.map(|fee| utils::u256_to_bytes(fee).to_vec()), .map(|fee| utils::u256_to_bytes(fee).to_vec()),
max_priority_fee_per_gas: full_grant max_priority_fee_per_gas: full_grant
.basic .basic
.max_priority_fee_per_gas .max_priority_fee_per_gas
.map(|fee| utils::u256_to_bytes(fee).to_vec()), .map(|fee| utils::u256_to_bytes(fee).to_vec()),
rate_limit_count: full_grant rate_limit_count: full_grant
.basic .basic
.rate_limit .rate_limit
.as_ref() .as_ref()
.map(|rl| rl.count as i32), .map(|rl| rl.count as i32),
rate_limit_window_secs: full_grant rate_limit_window_secs: full_grant
.basic .basic
.rate_limit .rate_limit
.as_ref() .as_ref()
.map(|rl| rl.window.num_seconds() as i32), .map(|rl| rl.window.num_seconds() as i32),
revoked_at: None, revoked_at: None,
}) })
.returning(evm_basic_grant::all_columns) .returning(evm_basic_grant::all_columns)
.get_result(conn) .get_result(conn)
.await?; .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) Ok(id)
} }
async fn perform_transaction<P: Policy>(&self, _context: EvalContext, _meaning: &P::Meaning) { pub async fn evaluate_transaction<P: Policy>(
}
pub async fn analyze_transaction(
&self, &self,
wallet_id: i32, wallet_id: i32,
client_id: i32, client_id: i32,
transaction: TxEip1559, transaction: TxEip1559,
) -> Result<SpecificMeaning, AnalyzeError> { // don't log
dry_run: bool,
) -> Result<(), VetError> {
let TxKind::Call(to) = transaction.to else { let TxKind::Call(to) = transaction.to else {
return Err(AnalyzeError::ContractCreationNotSupported); return Err(VetError::ContractCreationNotSupported);
}; };
let context = policies::EvalContext { let context = policies::EvalContext {
wallet_id, wallet_id,
@@ -124,10 +203,17 @@ impl Engine {
calldata: transaction.input.clone(), calldata: transaction.input.clone(),
}; };
if let Some(meaning) = EtherTransfer::analyze(&context) { if let Some(meaning) = EtherTransfer::analyze(&context) {
return Ok(SpecificMeaning::EtherTransfer(meaning)); return self
.vet_transaction::<EtherTransfer>(context, &meaning, dry_run)
.await;
} }
Err(AnalyzeError::UnsupportedTransactionType) if let Some(meaning) = TokenTransfer::analyze(&context) {
return self
.vet_transaction::<TokenTransfer>(context, &meaning, dry_run)
.await;
}
Err(VetError::UnsupportedTransactionType)
} }
} }

View File

@@ -3,8 +3,7 @@ use std::fmt::Display;
use alloy::primitives::{Address, Bytes, ChainId, U256}; use alloy::primitives::{Address, Bytes, ChainId, U256};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use diesel::{ use diesel::{
ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite,
sqlite::Sqlite,
}; };
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
use miette::Diagnostic; use miette::Diagnostic;
@@ -70,8 +69,8 @@ pub struct Grant<PolicySettings> {
} }
pub trait Policy: Sized { pub trait Policy: Sized {
type Settings: Send + 'static + Into<SpecificGrant>; type Settings: Send + Sync + 'static + Into<SpecificGrant>;
type Meaning: Display + Send + 'static + Into<SpecificMeaning>; type Meaning: Display + Send + Sync + 'static + Into<SpecificMeaning>;
fn analyze(context: &EvalContext) -> Option<Self::Meaning>; fn analyze(context: &EvalContext) -> Option<Self::Meaning>;
@@ -106,7 +105,7 @@ pub trait Policy: Sized {
log_id: i32, log_id: i32,
grant: &Grant<Self::Settings>, grant: &Grant<Self::Settings>,
conn: &mut impl AsyncConnection<Backend = Sqlite>, conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> impl Future<Output = QueryResult<()>>; ) -> impl Future<Output = QueryResult<()>> + Send;
} }
pub enum ReceiverTarget { pub enum ReceiverTarget {

View File

@@ -114,8 +114,8 @@ async fn check_volume_rate_limits(
Ok(violations) Ok(violations)
} }
pub struct TokenTransferPolicy; pub struct TokenTransfer;
impl Policy for TokenTransferPolicy { impl Policy for TokenTransfer {
type Settings = Settings; type Settings = Settings;
type Meaning = Meaning; type Meaning = Meaning;