feat(evm): add grant management and transaction signing
This commit is contained in:
@@ -1,17 +1,56 @@
|
||||
use alloy::primitives::Address;
|
||||
use diesel::{QueryDsl, SelectableHelper as _, dsl::insert_into};
|
||||
use alloy::{consensus::TxEip1559, network::TxSigner, primitives::Address, signers::Signature};
|
||||
use diesel::{ExpressionMethods, OptionalExtension as _, QueryDsl, SelectableHelper as _, dsl::insert_into};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::{Actor, actor::ActorRef, messages};
|
||||
use memsafe::MemSafe;
|
||||
use rand::{SeedableRng, rng, rngs::StdRng};
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::{CreateNew, KeyHolder},
|
||||
db::{self, DatabasePool, models, schema},
|
||||
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
||||
db::{self, DatabasePool, models::{self, EvmBasicGrant, SqliteTimestamp}, schema},
|
||||
evm::{
|
||||
self, RunKind,
|
||||
policies::{
|
||||
FullGrant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
||||
ether_transfer::EtherTransfer,
|
||||
token_transfers::TokenTransfer,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub use crate::evm::safe_signer;
|
||||
|
||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||
pub enum SignTransactionError {
|
||||
#[error("Wallet not found")]
|
||||
#[diagnostic(code(arbiter::evm::sign::wallet_not_found))]
|
||||
WalletNotFound,
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::sign::database))]
|
||||
Database(#[from] diesel::result::Error),
|
||||
|
||||
#[error("Database pool error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::sign::pool))]
|
||||
Pool(#[from] db::PoolError),
|
||||
|
||||
#[error("Keyholder error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::sign::keyholder))]
|
||||
Keyholder(#[from] crate::actors::keyholder::Error),
|
||||
|
||||
#[error("Keyholder mailbox error")]
|
||||
#[diagnostic(code(arbiter::evm::sign::keyholder_send))]
|
||||
KeyholderSend,
|
||||
|
||||
#[error("Signing error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::sign::signing))]
|
||||
Signing(#[from] alloy::signers::Error),
|
||||
|
||||
#[error("Policy error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::sign::vet))]
|
||||
Vet(#[from] evm::VetError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||
pub enum Error {
|
||||
#[error("Keyholder error: {0}")]
|
||||
@@ -29,6 +68,10 @@ pub enum Error {
|
||||
#[error("Database pool error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::database_pool))]
|
||||
DatabasePool(#[from] db::PoolError),
|
||||
|
||||
#[error("Grant creation error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::creation))]
|
||||
Creation(#[from] evm::CreationError),
|
||||
}
|
||||
|
||||
#[derive(Actor)]
|
||||
@@ -36,6 +79,7 @@ pub struct EvmActor {
|
||||
pub keyholder: ActorRef<KeyHolder>,
|
||||
pub db: DatabasePool,
|
||||
pub rng: StdRng,
|
||||
pub engine: evm::Engine,
|
||||
}
|
||||
|
||||
impl EvmActor {
|
||||
@@ -43,7 +87,8 @@ impl EvmActor {
|
||||
// is it safe to seed rng from system once?
|
||||
// todo: audit
|
||||
let rng = StdRng::from_rng(&mut rng());
|
||||
Self { keyholder, db, rng }
|
||||
let engine = evm::Engine::new(db.clone());
|
||||
Self { keyholder, db, rng, engine }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,3 +136,111 @@ impl EvmActor {
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[messages]
|
||||
impl EvmActor {
|
||||
#[message]
|
||||
pub async fn useragent_create_grant(
|
||||
&mut self,
|
||||
client_id: i32,
|
||||
basic: SharedGrantSettings,
|
||||
grant: SpecificGrant,
|
||||
) -> Result<i32, evm::CreationError> {
|
||||
match grant {
|
||||
SpecificGrant::EtherTransfer(settings) => {
|
||||
self.engine
|
||||
.create_grant::<EtherTransfer>(client_id, FullGrant { basic, specific: settings })
|
||||
.await
|
||||
}
|
||||
SpecificGrant::TokenTransfer(settings) => {
|
||||
self.engine
|
||||
.create_grant::<TokenTransfer>(client_id, FullGrant { basic, specific: settings })
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
|
||||
let mut conn = self.db.get().await?;
|
||||
diesel::update(schema::evm_basic_grant::table)
|
||||
.filter(schema::evm_basic_grant::id.eq(grant_id))
|
||||
.set(schema::evm_basic_grant::revoked_at.eq(SqliteTimestamp::now()))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn useragent_list_grants(
|
||||
&mut self,
|
||||
wallet_id: Option<i32>,
|
||||
) -> Result<Vec<EvmBasicGrant>, Error> {
|
||||
let mut conn = self.db.get().await?;
|
||||
let mut query = schema::evm_basic_grant::table
|
||||
.select(EvmBasicGrant::as_select())
|
||||
.filter(schema::evm_basic_grant::revoked_at.is_null())
|
||||
.into_boxed();
|
||||
if let Some(wid) = wallet_id {
|
||||
query = query.filter(schema::evm_basic_grant::wallet_id.eq(wid));
|
||||
}
|
||||
Ok(query.load(&mut conn).await?)
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn shared_analyze_transaction(
|
||||
&mut self,
|
||||
client_id: i32,
|
||||
wallet_address: Address,
|
||||
transaction: TxEip1559,
|
||||
) -> Result<SpecificMeaning, SignTransactionError> {
|
||||
let mut conn = self.db.get().await?;
|
||||
let wallet = schema::evm_wallet::table
|
||||
.select(models::EvmWallet::as_select())
|
||||
.filter(schema::evm_wallet::address.eq(wallet_address.as_slice()))
|
||||
.first(&mut conn)
|
||||
.await
|
||||
.optional()?
|
||||
.ok_or(SignTransactionError::WalletNotFound)?;
|
||||
drop(conn);
|
||||
|
||||
let meaning = self.engine
|
||||
.evaluate_transaction(wallet.id, client_id, transaction.clone(), RunKind::Execution)
|
||||
.await?;
|
||||
|
||||
Ok(meaning)
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn client_sign_transaction(
|
||||
&mut self,
|
||||
client_id: i32,
|
||||
wallet_address: Address,
|
||||
mut transaction: TxEip1559,
|
||||
) -> Result<Signature, SignTransactionError> {
|
||||
let mut conn = self.db.get().await?;
|
||||
let wallet = schema::evm_wallet::table
|
||||
.select(models::EvmWallet::as_select())
|
||||
.filter(schema::evm_wallet::address.eq(wallet_address.as_slice()))
|
||||
.first(&mut conn)
|
||||
.await
|
||||
.optional()?
|
||||
.ok_or(SignTransactionError::WalletNotFound)?;
|
||||
drop(conn);
|
||||
|
||||
let raw_key: MemSafe<Vec<u8>> = self
|
||||
.keyholder
|
||||
.ask(Decrypt { aead_id: wallet.aead_encrypted_id })
|
||||
.await
|
||||
.map_err(|_| SignTransactionError::KeyholderSend)?;
|
||||
|
||||
let signer = safe_signer::SafeSigner::from_memsafe(raw_key)?;
|
||||
|
||||
self.engine
|
||||
.evaluate_transaction(wallet.id, client_id, transaction.clone(), RunKind::Execution)
|
||||
.await?;
|
||||
|
||||
use alloy::network::TxSignerSync as _;
|
||||
Ok(signer.sign_transaction_sync(&mut transaction)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,15 @@ pub enum CreationError {
|
||||
Database(#[from] diesel::result::Error),
|
||||
}
|
||||
|
||||
/// Controls whether a transaction should be executed or only validated
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RunKind {
|
||||
/// Validate and record the transaction
|
||||
Execution,
|
||||
/// Validate only, do not record
|
||||
CheckOnly,
|
||||
}
|
||||
|
||||
// Supporting only EIP-1559 transactions for now, but we can easily extend this to support legacy transactions if needed
|
||||
pub struct Engine {
|
||||
db: db::DatabasePool,
|
||||
@@ -97,7 +106,7 @@ impl Engine {
|
||||
&self,
|
||||
context: EvalContext,
|
||||
meaning: &P::Meaning,
|
||||
dry_run: bool,
|
||||
run_kind: RunKind,
|
||||
) -> Result<(), PolicyError> {
|
||||
let mut conn = self.db.get().await?;
|
||||
|
||||
@@ -108,7 +117,7 @@ impl Engine {
|
||||
let violations = P::evaluate(&context, meaning, &grant, &mut conn).await?;
|
||||
if !violations.is_empty() {
|
||||
return Err(PolicyError::Violations(violations));
|
||||
} else if !dry_run {
|
||||
} else if run_kind == RunKind::Execution {
|
||||
conn.transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
let log_id: i32 = insert_into(evm_transaction_log::table)
|
||||
@@ -192,13 +201,12 @@ impl Engine {
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub async fn evaluate_transaction<P: Policy>(
|
||||
pub async fn evaluate_transaction(
|
||||
&self,
|
||||
wallet_id: i32,
|
||||
client_id: i32,
|
||||
transaction: TxEip1559,
|
||||
// don't log
|
||||
dry_run: bool,
|
||||
run_kind: RunKind,
|
||||
) -> Result<SpecificMeaning, VetError> {
|
||||
let TxKind::Call(to) = transaction.to else {
|
||||
return Err(VetError::ContractCreationNotSupported);
|
||||
@@ -214,7 +222,7 @@ impl Engine {
|
||||
|
||||
if let Some(meaning) = EtherTransfer::analyze(&context) {
|
||||
return match self
|
||||
.vet_transaction::<EtherTransfer>(context, &meaning, dry_run)
|
||||
.vet_transaction::<EtherTransfer>(context, &meaning, run_kind)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(meaning.into()),
|
||||
@@ -223,7 +231,7 @@ impl Engine {
|
||||
}
|
||||
if let Some(meaning) = TokenTransfer::analyze(&context) {
|
||||
return match self
|
||||
.vet_transaction::<TokenTransfer>(context, &meaning, dry_run)
|
||||
.vet_transaction::<TokenTransfer>(context, &meaning, run_kind)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(meaning.into()),
|
||||
|
||||
@@ -59,6 +59,18 @@ pub fn generate(rng: &mut impl rand::Rng) -> (MemSafe<[u8; 32]>, Address) {
|
||||
}
|
||||
|
||||
impl SafeSigner {
|
||||
/// Reconstructs a `SafeSigner` from key material held in a [`MemSafe`] buffer.
|
||||
///
|
||||
/// The key bytes are read from protected memory, parsed as a secp256k1
|
||||
/// scalar, and immediately moved into a new [`MemSafe`] cell. The raw
|
||||
/// bytes are never exposed outside this function.
|
||||
pub fn from_memsafe(mut cell: MemSafe<Vec<u8>>) -> Result<Self> {
|
||||
let reader = cell.read().map_err(Error::other)?;
|
||||
let sk = SigningKey::from_slice(reader.as_slice()).map_err(Error::other)?;
|
||||
drop(reader);
|
||||
Self::new(sk)
|
||||
}
|
||||
|
||||
/// Creates a new `SafeSigner` by moving the signing key into a protected
|
||||
/// memory region.
|
||||
pub fn new(key: SigningKey) -> Result<Self> {
|
||||
|
||||
Reference in New Issue
Block a user