diff --git a/protobufs/client.proto b/protobufs/client.proto index 83d25cf..384851f 100644 --- a/protobufs/client.proto +++ b/protobufs/client.proto @@ -49,6 +49,7 @@ message ClientRequest { AuthChallengeRequest auth_challenge_request = 1; AuthChallengeSolution auth_challenge_solution = 2; google.protobuf.Empty query_vault_state = 3; + arbiter.evm.EvmSignTransactionRequest evm_sign_transaction = 5; } } diff --git a/protobufs/user_agent.proto b/protobufs/user_agent.proto index 79e0f2c..f08aeea 100644 --- a/protobufs/user_agent.proto +++ b/protobufs/user_agent.proto @@ -154,6 +154,11 @@ message ListWalletAccessResponse { repeated SdkClientWalletAccess accesses = 1; } +message UserAgentEvmSignTransactionRequest { + int32 client_id = 1; + arbiter.evm.EvmSignTransactionRequest request = 2; +} + message UserAgentRequest { int32 id = 16; oneof payload { @@ -174,6 +179,7 @@ message UserAgentRequest { SdkClientGrantWalletAccess grant_wallet_access = 15; SdkClientRevokeWalletAccess revoke_wallet_access = 17; google.protobuf.Empty list_wallet_access = 18; + UserAgentEvmSignTransactionRequest evm_sign_transaction = 19; } } message UserAgentResponse { @@ -195,5 +201,6 @@ message UserAgentResponse { SdkClientListResponse sdk_client_list_response = 14; BootstrapResult bootstrap_result = 15; ListWalletAccessResponse list_wallet_access_response = 17; + arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 18; } } diff --git a/server/crates/arbiter-client/src/transport.rs b/server/crates/arbiter-client/src/transport.rs index d56a9f8..7332e89 100644 --- a/server/crates/arbiter-client/src/transport.rs +++ b/server/crates/arbiter-client/src/transport.rs @@ -1,6 +1,4 @@ -use arbiter_proto::proto::{ - client::{ClientRequest, ClientResponse}, -}; +use arbiter_proto::proto::client::{ClientRequest, ClientResponse}; use std::sync::atomic::{AtomicI32, Ordering}; use tokio::sync::mpsc; @@ -36,9 +34,7 @@ impl ClientTransport { .map_err(|_| ClientSignError::ChannelClosed) } - pub(crate) async fn recv( - &mut self, - ) -> std::result::Result { + pub(crate) async fn recv(&mut self) -> std::result::Result { match self.receiver.message().await { Ok(Some(resp)) => Ok(resp), Ok(None) => Err(ClientSignError::ConnectionClosed), diff --git a/server/crates/arbiter-client/src/wallets/evm.rs b/server/crates/arbiter-client/src/wallets/evm.rs index 32ae735..4533793 100644 --- a/server/crates/arbiter-client/src/wallets/evm.rs +++ b/server/crates/arbiter-client/src/wallets/evm.rs @@ -8,7 +8,15 @@ use async_trait::async_trait; use std::sync::Arc; use tokio::sync::Mutex; -use crate::transport::ClientTransport; +use arbiter_proto::proto::{ + client::{ + ClientRequest, client_request::Payload as ClientRequestPayload, + client_response::Payload as ClientResponsePayload, + }, + evm::evm_sign_transaction_response::Result as EvmSignTransactionResult, +}; + +use crate::transport::{ClientTransport, next_request_id}; pub struct ArbiterEvmWallet { transport: Arc>, @@ -79,11 +87,61 @@ impl TxSigner for ArbiterEvmWallet { &self, tx: &mut dyn SignableTransaction, ) -> Result { - let _transport = self.transport.lock().await; self.validate_chain_id(tx)?; - Err(Error::other( - "transaction signing is not supported by current arbiter.client protocol", - )) + let mut transport = self.transport.lock().await; + let request_id = next_request_id(); + let rlp_transaction = tx.encoded_for_signing(); + + transport + .send(ClientRequest { + request_id, + payload: Some(ClientRequestPayload::EvmSignTransaction( + arbiter_proto::proto::evm::EvmSignTransactionRequest { + wallet_address: self.address.to_vec(), + rlp_transaction, + }, + )), + }) + .await + .map_err(|_| Error::other("failed to send evm sign transaction request"))?; + + let response = transport + .recv() + .await + .map_err(|_| Error::other("failed to receive evm sign transaction response"))?; + + if response.request_id != Some(request_id) { + return Err(Error::other( + "received mismatched response id for evm sign transaction", + )); + } + + let payload = response + .payload + .ok_or_else(|| Error::other("missing evm sign transaction response payload"))?; + + let ClientResponsePayload::EvmSignTransaction(response) = payload else { + return Err(Error::other( + "unexpected response payload for evm sign transaction request", + )); + }; + + let result = response + .result + .ok_or_else(|| Error::other("missing evm sign transaction result"))?; + + match result { + EvmSignTransactionResult::Signature(signature) => { + Signature::try_from(signature.as_slice()) + .map_err(|_| Error::other("invalid signature returned by server")) + } + EvmSignTransactionResult::EvalError(eval_error) => Err(Error::other(format!( + "transaction rejected by policy: {eval_error:?}" + ))), + EvmSignTransactionResult::Error(code) => Err(Error::other(format!( + "server failed to sign transaction with error code {code}" + ))), + } } } diff --git a/server/crates/arbiter-server/src/actors/client/auth.rs b/server/crates/arbiter-server/src/actors/client/auth.rs index 4fd53bf..181b974 100644 --- a/server/crates/arbiter-server/src/actors/client/auth.rs +++ b/server/crates/arbiter-server/src/actors/client/auth.rs @@ -1,5 +1,6 @@ use arbiter_proto::{ - ClientMetadata, format_challenge, transport::{Bi, expect_message} + ClientMetadata, format_challenge, + transport::{Bi, expect_message}, }; use chrono::Utc; use diesel::{ @@ -83,7 +84,6 @@ async fn get_client_and_nonce( })?; conn.exclusive_transaction(|conn| { - let pubkey_bytes = pubkey_bytes.clone(); Box::pin(async move { let Some((client_id, current_nonce)) = program_client::table .filter(program_client::public_key.eq(&pubkey_bytes)) @@ -290,7 +290,7 @@ where pub async fn authenticate( props: &mut ClientConnection, transport: &mut T, -) -> Result +) -> Result where T: Bi> + Send + ?Sized, { @@ -318,7 +318,6 @@ where }; sync_client_metadata(&props.db, info.id, &metadata).await?; - challenge_client(transport, pubkey, info.current_nonce).await?; transport @@ -329,5 +328,5 @@ where Error::Transport })?; - Ok(pubkey) + Ok(info.id) } diff --git a/server/crates/arbiter-server/src/actors/client/mod.rs b/server/crates/arbiter-server/src/actors/client/mod.rs index f60e90a..f747572 100644 --- a/server/crates/arbiter-server/src/actors/client/mod.rs +++ b/server/crates/arbiter-server/src/actors/client/mod.rs @@ -3,7 +3,7 @@ use kameo::actor::Spawn; use tracing::{error, info}; use crate::{ - actors::{GlobalActors, client::{ session::ClientSession}}, + actors::{GlobalActors, client::session::ClientSession}, db, }; @@ -20,7 +20,10 @@ pub struct ClientConnection { impl ClientConnection { pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self { - Self { db, actors } + Self { + db, + actors, + } } } @@ -32,8 +35,8 @@ where T: Bi> + Send + ?Sized, { match auth::authenticate(&mut props, transport).await { - Ok(_pubkey) => { - ClientSession::spawn(ClientSession::new(props)); + Ok(client_id) => { + ClientSession::spawn(ClientSession::new(props, client_id)); info!("Client authenticated, session started"); } Err(err) => { diff --git a/server/crates/arbiter-server/src/actors/client/session.rs b/server/crates/arbiter-server/src/actors/client/session.rs index 83e2e29..d243f00 100644 --- a/server/crates/arbiter-server/src/actors/client/session.rs +++ b/server/crates/arbiter-server/src/actors/client/session.rs @@ -1,21 +1,30 @@ +use ed25519_dalek::VerifyingKey; use kameo::{Actor, messages}; use tracing::error; +use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; + use crate::{ actors::{ - GlobalActors, client::ClientConnection, flow_coordinator::RegisterClient, + GlobalActors, + client::ClientConnection, flow_coordinator::RegisterClient, + + evm::{ClientSignTransaction, SignTransactionError}, keyholder::KeyHolderState, + }, db, + evm::VetError, }; pub struct ClientSession { props: ClientConnection, + client_id: i32, } impl ClientSession { - pub(crate) fn new(props: ClientConnection) -> Self { - Self { props } + pub(crate) fn new(props: ClientConnection, client_id: i32) -> Self { + Self { props, client_id } } } @@ -35,6 +44,34 @@ impl ClientSession { Ok(vault_state) } + + #[message] + pub(crate) async fn handle_sign_transaction( + &mut self, + wallet_address: Address, + transaction: TxEip1559, + ) -> Result { + match self + .props + .actors + .evm + .ask(ClientSignTransaction { + client_id: self.client_id, + wallet_address, + transaction, + }) + .await + { + Ok(signature) => Ok(signature), + Err(kameo::error::SendError::HandlerError(SignTransactionError::Vet(vet_error))) => { + Err(SignTransactionRpcError::Vet(vet_error)) + } + Err(err) => { + error!(?err, "Failed to sign EVM transaction in client session"); + Err(SignTransactionRpcError::Internal) + } + } + } } impl Actor for ClientSession { @@ -59,7 +96,7 @@ impl Actor for ClientSession { impl ClientSession { pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self { let props = ClientConnection::new(db, actors); - Self { props } + Self { props, client_id: 0 } } } @@ -70,3 +107,12 @@ pub enum Error { #[error("Internal error")] Internal, } + +#[derive(Debug, thiserror::Error)] +pub enum SignTransactionRpcError { + #[error("Policy evaluation failed")] + Vet(#[from] VetError), + + #[error("Internal error")] + Internal, +} diff --git a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs index 30b10ae..f295c73 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs @@ -1,6 +1,6 @@ use std::sync::Mutex; -use alloy::primitives::Address; +use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; use diesel::sql_types::ops::Add; use diesel::{BoolExpressionMethods as _, ExpressionMethods as _, QueryDsl as _, SelectableHelper}; @@ -23,7 +23,8 @@ use crate::safe_cell::SafeCell; use crate::{ actors::{ evm::{ - Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants, + ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError, + UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants, }, keyholder::{self, Bootstrap, TryUnseal}, user_agent::session::{ @@ -112,6 +113,15 @@ pub enum BootstrapError { General(#[from] super::Error), } +#[derive(Debug, Error)] +pub enum SignTransactionError { + #[error("Policy evaluation failed")] + Vet(#[from] crate::evm::VetError), + + #[error("Internal signing error")] + Internal, +} + #[messages] impl UserAgentSession { #[message] @@ -356,6 +366,35 @@ impl UserAgentSession { } } + #[message] + pub(crate) async fn handle_sign_transaction( + &mut self, + client_id: i32, + wallet_address: Address, + transaction: TxEip1559, + ) -> Result { + match self + .props + .actors + .evm + .ask(ClientSignTransaction { + client_id, + wallet_address, + transaction, + }) + .await + { + Ok(signature) => Ok(signature), + Err(SendError::HandlerError(EvmSignError::Vet(vet_error))) => { + Err(SignTransactionError::Vet(vet_error)) + } + Err(err) => { + error!(?err, "EVM sign transaction failed in user-agent session"); + Err(SignTransactionError::Internal) + } + } + } + #[message] pub(crate) async fn handle_grant_evm_wallet_access( &mut self, diff --git a/server/crates/arbiter-server/src/evm/mod.rs b/server/crates/arbiter-server/src/evm/mod.rs index 54bcb1e..c922202 100644 --- a/server/crates/arbiter-server/src/evm/mod.rs +++ b/server/crates/arbiter-server/src/evm/mod.rs @@ -32,7 +32,7 @@ mod utils; #[derive(Debug, thiserror::Error, miette::Diagnostic)] pub enum PolicyError { #[error("Database error")] - Error(#[from] crate::db::DatabaseError), + Database(#[from] crate::db::DatabaseError), #[error("Transaction violates policy: {0:?}")] #[diagnostic(code(arbiter_server::evm::policy_error::violation))] Violations(Vec), diff --git a/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs b/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs index e823f07..d419b59 100644 --- a/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs +++ b/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs @@ -36,8 +36,8 @@ use super::{DatabaseID, EvalContext, EvalViolation}; // Plain ether transfer #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Meaning { - to: Address, - value: U256, + pub(crate) to: Address, + pub(crate) value: U256, } impl Display for Meaning { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs b/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs index 7dfec70..cef49d9 100644 --- a/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs +++ b/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs @@ -38,9 +38,9 @@ fn grant_join() -> _ { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Meaning { - token: &'static TokenInfo, - to: Address, - value: U256, + pub(crate) token: &'static TokenInfo, + pub(crate) to: Address, + pub(crate) value: U256, } impl std::fmt::Display for Meaning { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/server/crates/arbiter-server/src/grpc/client.rs b/server/crates/arbiter-server/src/grpc/client.rs index cd032f4..4e024ed 100644 --- a/server/crates/arbiter-server/src/grpc/client.rs +++ b/server/crates/arbiter-server/src/grpc/client.rs @@ -1,8 +1,15 @@ +use alloy::primitives::Address; use arbiter_proto::{ - proto::client::{ - ClientRequest, ClientResponse, VaultState as ProtoVaultState, - client_request::Payload as ClientRequestPayload, - client_response::Payload as ClientResponsePayload, + proto::{ + client::{ + ClientRequest, ClientResponse, VaultState as ProtoVaultState, + client_request::Payload as ClientRequestPayload, + client_response::Payload as ClientResponsePayload, + }, + evm::{ + EvmError as ProtoEvmError, EvmSignTransactionResponse, + evm_sign_transaction_response::Result as EvmSignTransactionResult, + }, }, transport::{Receiver, Sender, grpc::GrpcBi}, }; @@ -17,11 +24,18 @@ use crate::{ actors::{ client::{ self, ClientConnection, - session::{ClientSession, Error, HandleQueryVaultState}, + session::{ + ClientSession, Error, HandleQueryVaultState, HandleSignTransaction, + SignTransactionRpcError, + }, }, keyholder::KeyHolderState, }, - grpc::request_tracker::RequestTracker, + grpc::{ + Convert, TryConvert, + common::inbound::{RawEvmAddress, RawEvmTransaction}, + request_tracker::RequestTracker, + }, }; mod auth; @@ -34,7 +48,9 @@ async fn dispatch_loop( mut request_tracker: RequestTracker, ) { loop { - let Some(message) = bi.recv().await else { return }; + let Some(message) = bi.recv().await else { + return; + }; let conn = match message { Ok(conn) => conn, @@ -53,16 +69,24 @@ async fn dispatch_loop( }; let Some(payload) = conn.payload else { - let _ = bi.send(Err(Status::invalid_argument("Missing client request payload"))).await; + let _ = bi + .send(Err(Status::invalid_argument( + "Missing client request payload", + ))) + .await; return; }; match dispatch_inner(&actor, payload).await { Ok(response) => { - if bi.send(Ok(ClientResponse { - request_id: Some(request_id), - payload: Some(response), - })).await.is_err() { + if bi + .send(Ok(ClientResponse { + request_id: Some(request_id), + payload: Some(response), + })) + .await + .is_err() + { return; } } @@ -92,6 +116,47 @@ async fn dispatch_inner( }; Ok(ClientResponsePayload::VaultState(state.into())) } + ClientRequestPayload::EvmSignTransaction(request) => { + let address: Address = RawEvmAddress(request.wallet_address).try_convert()?; + let transaction = RawEvmTransaction(request.rlp_transaction).try_convert()?; + + let response = match actor + .ask(HandleSignTransaction { + wallet_address: address, + transaction, + }) + .await + { + Ok(signature) => EvmSignTransactionResponse { + result: Some(EvmSignTransactionResult::Signature( + signature.as_bytes().to_vec(), + )), + }, + Err(kameo::error::SendError::HandlerError(SignTransactionRpcError::Vet( + vet_error, + ))) => EvmSignTransactionResponse { + result: Some(vet_error.convert()), + }, + + Err(kameo::error::SendError::HandlerError(SignTransactionRpcError::Internal)) => { + EvmSignTransactionResponse { + result: Some(EvmSignTransactionResult::Error( + ProtoEvmError::Internal.into(), + )), + } + } + Err(err) => { + warn!(error = ?err, "Failed to sign EVM transaction"); + EvmSignTransactionResponse { + result: Some(EvmSignTransactionResult::Error( + ProtoEvmError::Internal.into(), + )), + } + } + }; + + Ok(ClientResponsePayload::EvmSignTransaction(response)) + } payload => { warn!(?payload, "Unsupported post-auth client request"); Err(Status::invalid_argument("Unsupported client request")) @@ -102,14 +167,21 @@ async fn dispatch_inner( pub async fn start(mut conn: ClientConnection, mut bi: GrpcBi) { let mut request_tracker = RequestTracker::default(); - if let Err(e) = auth::start(&mut conn, &mut bi, &mut request_tracker).await { - let mut transport = auth::AuthTransportAdapter::new(&mut bi, &mut request_tracker); - let _ = transport.send(Err(e.clone())).await; - warn!(error = ?e, "Client authentication failed"); - return; + let client_id = match auth::start(&mut conn, &mut bi, &mut request_tracker).await { + Ok(id) => id, + Err(err) => { + let _ = bi + .send(Err(Status::unauthenticated(format!( + "Authentication failed: {}", + err + )))) + .await; + warn!(error = ?err, "Client authentication failed"); + return; + } }; - let actor = client::session::ClientSession::spawn(client::session::ClientSession::new(conn)); + let actor = ClientSession::spawn(ClientSession::new(conn, client_id)); let actor_for_cleanup = actor.clone(); info!("Client authenticated successfully"); diff --git a/server/crates/arbiter-server/src/grpc/client/auth.rs b/server/crates/arbiter-server/src/grpc/client/auth.rs index c711520..a92a57d 100644 --- a/server/crates/arbiter-server/src/grpc/client/auth.rs +++ b/server/crates/arbiter-server/src/grpc/client/auth.rs @@ -1,11 +1,13 @@ use arbiter_proto::{ - ClientMetadata, proto::client::{ + ClientMetadata, + proto::client::{ AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest, AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult, ClientInfo as ProtoClientInfo, ClientRequest, ClientResponse, client_request::Payload as ClientRequestPayload, client_response::Payload as ClientResponsePayload, - }, transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi} + }, + transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}, }; use async_trait::async_trait; use tonic::Status; @@ -181,8 +183,7 @@ pub async fn start( conn: &mut ClientConnection, bi: &mut GrpcBi, request_tracker: &mut RequestTracker, -) -> Result<(), auth::Error> { +) -> Result { let mut transport = AuthTransportAdapter::new(bi, request_tracker); - client::auth::authenticate(conn, &mut transport).await?; - Ok(()) + client::auth::authenticate(conn, &mut transport).await } diff --git a/server/crates/arbiter-server/src/grpc/common.rs b/server/crates/arbiter-server/src/grpc/common.rs new file mode 100644 index 0000000..5756441 --- /dev/null +++ b/server/crates/arbiter-server/src/grpc/common.rs @@ -0,0 +1,2 @@ +pub mod inbound; +pub mod outbound; diff --git a/server/crates/arbiter-server/src/grpc/common/inbound.rs b/server/crates/arbiter-server/src/grpc/common/inbound.rs new file mode 100644 index 0000000..bf083f0 --- /dev/null +++ b/server/crates/arbiter-server/src/grpc/common/inbound.rs @@ -0,0 +1,36 @@ +use alloy::{consensus::TxEip1559, primitives::Address, rlp::Decodable as _}; + +use crate::grpc::TryConvert; + +pub struct RawEvmAddress(pub Vec); +impl TryConvert for RawEvmAddress { + type Output = Address; + + type Error = tonic::Status; + + fn try_convert(self) -> Result { + let wallet_address = match <[u8; 20]>::try_from(self.0.as_slice()) { + Ok(address) => Address::from(address), + Err(_) => { + return Err(tonic::Status::invalid_argument( + "Invalid EVM wallet address", + )); + } + }; + Ok(wallet_address) + } +} + +pub struct RawEvmTransaction(pub Vec); +impl TryConvert for RawEvmTransaction { + type Output = TxEip1559; + + type Error = tonic::Status; + + fn try_convert(mut self) -> Result { + let tx = TxEip1559::decode(&mut self.0.as_slice()).map_err(|_| { + tonic::Status::invalid_argument("Invalid EVM transaction format") + })?; + Ok(tx) + } +} \ No newline at end of file diff --git a/server/crates/arbiter-server/src/grpc/common/outbound.rs b/server/crates/arbiter-server/src/grpc/common/outbound.rs new file mode 100644 index 0000000..ac0f3d2 --- /dev/null +++ b/server/crates/arbiter-server/src/grpc/common/outbound.rs @@ -0,0 +1,114 @@ +use alloy::primitives::U256; +use arbiter_proto::proto::evm::{ + EvalViolation as ProtoEvalViolation, EvmError as ProtoEvmError, GasLimitExceededViolation, + NoMatchingGrantError, PolicyViolationsError, SpecificMeaning as ProtoSpecificMeaning, + TokenInfo as ProtoTokenInfo, TransactionEvalError as ProtoTransactionEvalError, + eval_violation::Kind as ProtoEvalViolationKind, + evm_sign_transaction_response::Result as EvmSignTransactionResult, + specific_meaning::Meaning as ProtoSpecificMeaningKind, + transaction_eval_error::Kind as ProtoTransactionEvalErrorKind, +}; + +use crate::{ + evm::{ + PolicyError, VetError, + policies::{EvalViolation, SpecificMeaning}, + }, + grpc::Convert, +}; + +fn u256_to_proto_bytes(value: U256) -> Vec { + value.to_be_bytes::<32>().to_vec() +} + +impl Convert for SpecificMeaning { + type Output = ProtoSpecificMeaning; + + fn convert(self) -> Self::Output { + let kind = match self { + SpecificMeaning::EtherTransfer(meaning) => ProtoSpecificMeaningKind::EtherTransfer( + arbiter_proto::proto::evm::EtherTransferMeaning { + to: meaning.to.to_vec(), + value: u256_to_proto_bytes(meaning.value), + }, + ), + SpecificMeaning::TokenTransfer(meaning) => ProtoSpecificMeaningKind::TokenTransfer( + arbiter_proto::proto::evm::TokenTransferMeaning { + token: Some(ProtoTokenInfo { + symbol: meaning.token.symbol.to_string(), + address: meaning.token.contract.to_vec(), + chain_id: meaning.token.chain, + }), + to: meaning.to.to_vec(), + value: u256_to_proto_bytes(meaning.value), + }, + ), + }; + + ProtoSpecificMeaning { + meaning: Some(kind), + } + } +} + +impl Convert for EvalViolation { + type Output = ProtoEvalViolation; + + fn convert(self) -> Self::Output { + let kind = match self { + EvalViolation::InvalidTarget { target } => { + ProtoEvalViolationKind::InvalidTarget(target.to_vec()) + } + EvalViolation::GasLimitExceeded { + max_gas_fee_per_gas, + max_priority_fee_per_gas, + } => ProtoEvalViolationKind::GasLimitExceeded(GasLimitExceededViolation { + max_gas_fee_per_gas: max_gas_fee_per_gas.map(u256_to_proto_bytes), + max_priority_fee_per_gas: max_priority_fee_per_gas.map(u256_to_proto_bytes), + }), + EvalViolation::RateLimitExceeded => ProtoEvalViolationKind::RateLimitExceeded(()), + EvalViolation::VolumetricLimitExceeded => { + ProtoEvalViolationKind::VolumetricLimitExceeded(()) + } + EvalViolation::InvalidTime => ProtoEvalViolationKind::InvalidTime(()), + EvalViolation::InvalidTransactionType => { + ProtoEvalViolationKind::InvalidTransactionType(()) + } + }; + + ProtoEvalViolation { kind: Some(kind) } + } +} + +impl Convert for VetError { + type Output = EvmSignTransactionResult; + + fn convert(self) -> Self::Output { + let kind = match self { + VetError::ContractCreationNotSupported => { + ProtoTransactionEvalErrorKind::ContractCreationNotSupported(()) + } + VetError::UnsupportedTransactionType => { + ProtoTransactionEvalErrorKind::UnsupportedTransactionType(()) + } + VetError::Evaluated(meaning, policy_error) => match policy_error { + PolicyError::NoMatchingGrant => { + ProtoTransactionEvalErrorKind::NoMatchingGrant(NoMatchingGrantError { + meaning: Some(meaning.convert()), + }) + } + PolicyError::Violations(violations) => { + ProtoTransactionEvalErrorKind::PolicyViolations(PolicyViolationsError { + meaning: Some(meaning.convert()), + violations: violations.into_iter().map(Convert::convert).collect(), + }) + } + PolicyError::Database(_) => { + return EvmSignTransactionResult::Error(ProtoEvmError::Internal.into()); + } + }, + }; + + EvmSignTransactionResult::EvalError(ProtoTransactionEvalError { kind: Some(kind) }.into()) + } +} diff --git a/server/crates/arbiter-server/src/grpc/mod.rs b/server/crates/arbiter-server/src/grpc/mod.rs index 149f0cb..7181594 100644 --- a/server/crates/arbiter-server/src/grpc/mod.rs +++ b/server/crates/arbiter-server/src/grpc/mod.rs @@ -14,10 +14,13 @@ use crate::{ grpc::user_agent::start, }; -pub mod client; mod request_tracker; + +pub mod client; pub mod user_agent; +mod common; + pub trait Convert { type Output; diff --git a/server/crates/arbiter-server/src/grpc/user_agent.rs b/server/crates/arbiter-server/src/grpc/user_agent.rs index 832c468..18f5aba 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent.rs @@ -6,10 +6,11 @@ use arbiter_proto::{ evm::{ EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse, EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, - GrantEntry, WalletCreateResponse, WalletEntry, WalletList, WalletListResponse, - evm_grant_create_response::Result as EvmGrantCreateResult, + EvmSignTransactionResponse, GrantEntry, WalletCreateResponse, WalletEntry, WalletList, + WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult, evm_grant_delete_response::Result as EvmGrantDeleteResult, evm_grant_list_response::Result as EvmGrantListResult, + evm_sign_transaction_response::Result as EvmSignTransactionResult, wallet_create_response::Result as WalletCreateResult, wallet_list_response::Result as WalletListResult, }, @@ -22,8 +23,8 @@ use arbiter_proto::{ SdkClientGrantWalletAccess, SdkClientList as ProtoSdkClientList, SdkClientListResponse as ProtoSdkClientListResponse, SdkClientRevokeWalletAccess, SdkClientWalletAccess, UnsealEncryptedKey as ProtoUnsealEncryptedKey, - UnsealResult as ProtoUnsealResult, UnsealStart, UserAgentRequest, UserAgentResponse, - VaultState as ProtoVaultState, + UnsealResult as ProtoUnsealResult, UnsealStart, UserAgentEvmSignTransactionRequest, + UserAgentRequest, UserAgentResponse, VaultState as ProtoVaultState, sdk_client_list_response::Result as ProtoSdkClientListResult, user_agent_request::Payload as UserAgentRequestPayload, user_agent_response::Payload as UserAgentResponsePayload, @@ -49,12 +50,24 @@ use crate::{ HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantEvmWalletAccess, HandleGrantList, HandleListWalletAccess, HandleNewClientApprove, HandleQueryVaultState, HandleRevokeEvmWalletAccess, - HandleSdkClientList, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, + HandleSdkClientList, HandleSignTransaction, HandleUnsealEncryptedKey, + HandleUnsealRequest, SignTransactionError as SessionSignTransactionError, + UnsealError, }, }, }, db::models::{CoreEvmWalletAccess, NewEvmWalletAccess}, - grpc::{Convert, TryConvert, request_tracker::RequestTracker}, + evm::{PolicyError, VetError, policies::EvalViolation}, + grpc::{ + Convert, TryConvert, + common::inbound::{RawEvmAddress, RawEvmTransaction}, + request_tracker::RequestTracker, + }, +}; +use alloy::{ + consensus::TxEip1559, + primitives::{Address, U256}, + rlp::Decodable, }; mod auth; mod inbound; @@ -178,7 +191,6 @@ async fn dispatch_inner( }, ) } - UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey { nonce, ciphertext, @@ -203,7 +215,6 @@ async fn dispatch_inner( }; UserAgentResponsePayload::UnsealResult(result.into()) } - UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey { nonce, ciphertext, @@ -231,7 +242,6 @@ async fn dispatch_inner( }; UserAgentResponsePayload::BootstrapResult(result.into()) } - UserAgentRequestPayload::QueryVaultState(_) => { let state = match actor.ask(HandleQueryVaultState {}).await { Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, @@ -244,7 +254,6 @@ async fn dispatch_inner( }; UserAgentResponsePayload::VaultState(state.into()) } - UserAgentRequestPayload::EvmWalletCreate(_) => { let result = match actor.ask(HandleEvmWalletCreate {}).await { Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry { @@ -260,7 +269,6 @@ async fn dispatch_inner( result: Some(result), }) } - UserAgentRequestPayload::EvmWalletList(_) => { let result = match actor.ask(HandleEvmWalletList {}).await { Ok(wallets) => WalletListResult::Wallets(WalletList { @@ -281,7 +289,6 @@ async fn dispatch_inner( result: Some(result), }) } - UserAgentRequestPayload::EvmGrantList(_) => { let result = match actor.ask(HandleGrantList {}).await { Ok(grants) => EvmGrantListResult::Grants(EvmGrantList { @@ -304,7 +311,6 @@ async fn dispatch_inner( result: Some(result), }) } - UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest { shared, specific }) => { let basic = shared .ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))? @@ -324,7 +330,6 @@ async fn dispatch_inner( result: Some(result), }) } - UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id }) => { let result = match actor.ask(HandleGrantDelete { grant_id }).await { Ok(()) => EvmGrantDeleteResult::Ok(()), @@ -337,7 +342,6 @@ async fn dispatch_inner( result: Some(result), }) } - UserAgentRequestPayload::SdkClientConnectionResponse(resp) => { let pubkey_bytes = <[u8; 32]>::try_from(resp.pubkey) .map_err(|_| Status::invalid_argument("Invalid Ed25519 public key length"))?; @@ -357,9 +361,7 @@ async fn dispatch_inner( return Ok(None); } - UserAgentRequestPayload::SdkClientRevoke(_) => todo!(), - UserAgentRequestPayload::SdkClientList(_) => { let result = match actor.ask(HandleSdkClientList {}).await { Ok(clients) => ProtoSdkClientListResult::Clients(ProtoSdkClientList { @@ -386,7 +388,6 @@ async fn dispatch_inner( result: Some(result), }) } - UserAgentRequestPayload::GrantWalletAccess(SdkClientGrantWalletAccess { accesses }) => { let entries: Vec = accesses.into_iter().map(|a| a.convert()).collect(); @@ -402,9 +403,11 @@ async fn dispatch_inner( } } } - UserAgentRequestPayload::RevokeWalletAccess(SdkClientRevokeWalletAccess { accesses }) => { - match actor.ask(HandleRevokeEvmWalletAccess { entries: accesses }).await { + match actor + .ask(HandleRevokeEvmWalletAccess { entries: accesses }) + .await + { Ok(()) => { info!("Successfully revoked wallet access"); return Ok(None); @@ -415,7 +418,6 @@ async fn dispatch_inner( } } } - UserAgentRequestPayload::ListWalletAccess(_) => { let result = match actor.ask(HandleListWalletAccess {}).await { Ok(accesses) => ListWalletAccessResponse { @@ -428,12 +430,59 @@ async fn dispatch_inner( }; UserAgentResponsePayload::ListWalletAccessResponse(result) } - UserAgentRequestPayload::AuthChallengeRequest(..) | UserAgentRequestPayload::AuthChallengeSolution(..) => { warn!(?payload, "Unsupported post-auth user agent request"); return Err(Status::invalid_argument("Unsupported user-agent request")); } + UserAgentRequestPayload::EvmSignTransaction(UserAgentEvmSignTransactionRequest { + client_id, + request, + }) => { + let Some(request) = request else { + warn!("Missing transaction signing request"); + return Err(Status::invalid_argument( + "Missing transaction signing request", + )); + }; + + let address: Address = RawEvmAddress(request.wallet_address).try_convert()?; + let transaction = RawEvmTransaction(request.rlp_transaction).try_convert()?; + + let response = match actor + .ask(HandleSignTransaction { + client_id, + wallet_address: address, + transaction, + }) + .await + { + Ok(signature) => EvmSignTransactionResponse { + result: Some(EvmSignTransactionResult::Signature( + signature.as_bytes().to_vec(), + )), + }, + Err(SendError::HandlerError(SessionSignTransactionError::Vet(vet_error))) => { + EvmSignTransactionResponse { result: Some(vet_error.convert()) } + } + Err(SendError::HandlerError(SessionSignTransactionError::Internal)) => { + EvmSignTransactionResponse { + result: Some(EvmSignTransactionResult::Error( + ProtoEvmError::Internal.into(), + )), + } + } + Err(err) => { + warn!(error = ?err, "Failed to sign EVM transaction via user-agent"); + EvmSignTransactionResponse { + result: Some(EvmSignTransactionResult::Error( + ProtoEvmError::Internal.into(), + )), + } + } + }; + UserAgentResponsePayload::EvmSignTransaction(response) + } }; Ok(Some(response))