feat(server::evm::engine): initial wiring of all components -- we now can evaluate transactions
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user