From 6987e5f70fb450a954e3046af97a382208f4c196 Mon Sep 17 00:00:00 2001 From: CleverWild Date: Thu, 26 Mar 2026 19:57:48 +0100 Subject: [PATCH] feat(evm): implement EVM sign transaction handling in client and user agent --- protobufs/client.proto | 1 + protobufs/user_agent.proto | 7 + server/crates/arbiter-client/src/transport.rs | 8 +- .../crates/arbiter-client/src/wallets/evm.rs | 68 +++++- .../arbiter-server/src/actors/client/auth.rs | 35 ++- .../arbiter-server/src/actors/client/mod.rs | 10 +- .../src/actors/client/session.rs | 46 +++- .../src/actors/user_agent/session.rs | 10 +- .../actors/user_agent/session/connection.rs | 49 +++- .../src/evm/policies/ether_transfer/mod.rs | 4 +- .../src/evm/policies/token_transfers/mod.rs | 6 +- .../crates/arbiter-server/src/grpc/client.rs | 180 +++++++++++++- .../arbiter-server/src/grpc/client/auth.rs | 7 +- .../arbiter-server/src/grpc/user_agent.rs | 225 +++++++++++++++++- 14 files changed, 605 insertions(+), 51 deletions(-) diff --git a/protobufs/client.proto b/protobufs/client.proto index c090a0d..126d99d 100644 --- a/protobufs/client.proto +++ b/protobufs/client.proto @@ -42,6 +42,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 fe41f87..79d8346 100644 --- a/protobufs/user_agent.proto +++ b/protobufs/user_agent.proto @@ -137,6 +137,11 @@ message SdkClientConnectionResponse { message SdkClientConnectionCancel {} +message UserAgentEvmSignTransactionRequest { + int32 client_id = 1; + arbiter.evm.EvmSignTransactionRequest request = 2; +} + message UserAgentRequest { int32 id = 16; oneof payload { @@ -155,6 +160,7 @@ message UserAgentRequest { SdkClientRevokeRequest sdk_client_revoke = 13; google.protobuf.Empty sdk_client_list = 14; BootstrapEncryptedKey bootstrap_encrypted_key = 15; + UserAgentEvmSignTransactionRequest evm_sign_transaction = 17; } } message UserAgentResponse { @@ -175,5 +181,6 @@ message UserAgentResponse { SdkClientRevokeResponse sdk_client_revoke_response = 13; SdkClientListResponse sdk_client_list_response = 14; BootstrapResult bootstrap_result = 15; + arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 17; } } 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 d957da8..bcf85d2 100644 --- a/server/crates/arbiter-server/src/actors/client/auth.rs +++ b/server/crates/arbiter-server/src/actors/client/auth.rs @@ -54,10 +54,19 @@ pub enum Outbound { AuthSuccess, } +#[derive(Debug, Clone)] +pub struct AuthenticatedClient { + pub pubkey: VerifyingKey, + pub client_id: i32, +} + /// Atomically reads and increments the nonce for a known client. /// Returns `None` if the pubkey is not registered. -async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result, Error> { - let pubkey_bytes = pubkey.as_bytes().to_vec(); +async fn get_nonce( + db: &db::DatabasePool, + pubkey: &VerifyingKey, +) -> Result, Error> { + let pubkey_bytes = pubkey.as_bytes(); let mut conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); @@ -65,7 +74,6 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result Result( props: &mut ClientConnection, transport: &mut T, -) -> Result +) -> Result where T: Bi> + Send + ?Sized, { - let Some(Inbound::AuthChallengeRequest { pubkey }) = transport.recv().await - else { + let Some(Inbound::AuthChallengeRequest { pubkey }) = transport.recv().await else { return Err(Error::Transport); }; - let nonce = match get_nonce(&props.db, &pubkey).await? { - Some(nonce) => nonce, + let (client_id, nonce) = match get_nonce(&props.db, &pubkey).await? { + Some(client_nonce) => client_nonce, None => { approve_new_client(&props.actors, pubkey).await?; match insert_client(&props.db, &pubkey).await? { - InsertClientResult::Inserted => 0, + InsertClientResult::Inserted => match get_nonce(&props.db, &pubkey).await? { + Some((client_id, _)) => (client_id, 0), + None => return Err(Error::DatabaseOperationFailed), + }, InsertClientResult::AlreadyExists => match get_nonce(&props.db, &pubkey).await? { - Some(nonce) => nonce, + Some((client_id, nonce)) => (client_id, nonce), None => return Err(Error::DatabaseOperationFailed), }, } @@ -245,5 +254,5 @@ where Error::Transport })?; - Ok(pubkey) + Ok(AuthenticatedClient { pubkey, client_id }) } diff --git a/server/crates/arbiter-server/src/actors/client/mod.rs b/server/crates/arbiter-server/src/actors/client/mod.rs index 3fae866..cf69ba7 100644 --- a/server/crates/arbiter-server/src/actors/client/mod.rs +++ b/server/crates/arbiter-server/src/actors/client/mod.rs @@ -10,11 +10,16 @@ use crate::{ pub struct ClientConnection { pub(crate) db: db::DatabasePool, pub(crate) actors: GlobalActors, + pub(crate) client_id: i32, } impl ClientConnection { pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self { - Self { db, actors } + Self { + db, + actors, + client_id: 0, + } } } @@ -26,7 +31,8 @@ where T: Bi> + Send + ?Sized, { match auth::authenticate(&mut props, transport).await { - Ok(_pubkey) => { + Ok(authenticated) => { + props.client_id = authenticated.client_id; ClientSession::spawn(ClientSession::new(props)); info!("Client authenticated, session started"); } diff --git a/server/crates/arbiter-server/src/actors/client/session.rs b/server/crates/arbiter-server/src/actors/client/session.rs index 93f2c6e..155c37d 100644 --- a/server/crates/arbiter-server/src/actors/client/session.rs +++ b/server/crates/arbiter-server/src/actors/client/session.rs @@ -1,11 +1,18 @@ use kameo::{Actor, messages}; use tracing::error; +use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; + use crate::{ actors::{ - GlobalActors, client::ClientConnection, keyholder::KeyHolderState, router::RegisterClient, + GlobalActors, + client::ClientConnection, + evm::{ClientSignTransaction, SignTransactionError}, + keyholder::KeyHolderState, + router::RegisterClient, }, db, + evm::VetError, }; pub struct ClientSession { @@ -34,6 +41,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.props.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 { @@ -69,3 +104,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.rs b/server/crates/arbiter-server/src/actors/user_agent/session.rs index b13bfd9..e719b18 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session.rs @@ -36,7 +36,10 @@ impl Error { pub struct UserAgentSession { props: UserAgentConnection, state: UserAgentStateMachine, - #[allow(dead_code, reason = "The session keeps ownership of the outbound transport even before the state-machine flow starts using it directly")] + #[allow( + dead_code, + reason = "The session keeps ownership of the outbound transport even before the state-machine flow starts using it directly" + )] sender: Box>, } @@ -44,8 +47,11 @@ mod connection; pub(crate) use connection::{ BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantList, HandleQueryVaultState, + HandleSignTransaction, +}; +pub use connection::{ + HandleUnsealEncryptedKey, HandleUnsealRequest, SignTransactionError, UnsealError, }; -pub use connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError}; impl UserAgentSession { pub(crate) fn new(props: UserAgentConnection, sender: Box>) -> Self { 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 44b47c3..49de59a 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 kameo::error::SendError; use kameo::messages; @@ -14,13 +14,14 @@ 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::{ - UserAgentSession, - state::{UnsealContext, UserAgentEvents, UserAgentStates}, - }, + UserAgentSession, + state::{UnsealContext, UserAgentEvents, UserAgentStates}, + }, }, safe_cell::SafeCellHandle as _, }; @@ -103,6 +104,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] @@ -351,4 +361,33 @@ 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) + } + } + } } 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 e77d994..2c43d05 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 34378ed..21799ea 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 2fb1d24..f384092 100644 --- a/server/crates/arbiter-server/src/grpc/client.rs +++ b/server/crates/arbiter-server/src/grpc/client.rs @@ -1,4 +1,5 @@ use arbiter_proto::{ + google::protobuf::Empty as ProtoEmpty, proto::client::{ ClientRequest, ClientResponse, VaultState as ProtoVaultState, client_request::Payload as ClientRequestPayload, @@ -17,16 +18,135 @@ use crate::{ actors::{ client::{ self, ClientConnection, - session::{ClientSession, Error, HandleQueryVaultState}, + session::{ + ClientSession, Error, HandleQueryVaultState, HandleSignTransaction, + SignTransactionRpcError, + }, }, keyholder::KeyHolderState, }, + evm::{PolicyError, VetError, policies::EvalViolation}, grpc::request_tracker::RequestTracker, utils::defer, }; +use alloy::{ + consensus::TxEip1559, + primitives::{Address, U256}, + rlp::Decodable, +}; +use arbiter_proto::proto::evm::{ + EvmError as ProtoEvmError, EvmSignTransactionResponse, EvalViolation as ProtoEvalViolation, + GasLimitExceededViolation, NoMatchingGrantError, PolicyViolationsError, + SpecificMeaning as ProtoSpecificMeaning, TokenInfo as ProtoTokenInfo, + TransactionEvalError, + evm_sign_transaction_response::Result as EvmSignTransactionResult, + eval_violation::Kind as ProtoEvalViolationKind, + specific_meaning::Meaning as ProtoSpecificMeaningKind, + transaction_eval_error::Kind as ProtoTransactionEvalErrorKind, +}; + mod auth; +fn u256_to_proto_bytes(value: U256) -> Vec { + value.to_be_bytes::<32>().to_vec() +} + +fn meaning_to_proto(meaning: crate::evm::policies::SpecificMeaning) -> ProtoSpecificMeaning { + let kind = match meaning { + crate::evm::policies::SpecificMeaning::EtherTransfer(meaning) => { + ProtoSpecificMeaningKind::EtherTransfer(arbiter_proto::proto::evm::EtherTransferMeaning { + to: meaning.to.to_vec(), + value: u256_to_proto_bytes(meaning.value), + }) + } + crate::evm::policies::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), + } +} + +fn violation_to_proto(violation: EvalViolation) -> ProtoEvalViolation { + let kind = match violation { + 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(ProtoEmpty {}), + EvalViolation::VolumetricLimitExceeded => { + ProtoEvalViolationKind::VolumetricLimitExceeded(ProtoEmpty {}) + } + EvalViolation::InvalidTime => ProtoEvalViolationKind::InvalidTime(ProtoEmpty {}), + EvalViolation::InvalidTransactionType => { + ProtoEvalViolationKind::InvalidTransactionType(ProtoEmpty {}) + } + }; + + ProtoEvalViolation { kind: Some(kind) } +} + +fn eval_error_to_proto(err: VetError) -> Option { + let kind = match err { + VetError::ContractCreationNotSupported => { + ProtoTransactionEvalErrorKind::ContractCreationNotSupported(ProtoEmpty {}) + } + VetError::UnsupportedTransactionType => { + ProtoTransactionEvalErrorKind::UnsupportedTransactionType(ProtoEmpty {}) + } + VetError::Evaluated(meaning, policy_error) => match policy_error { + PolicyError::NoMatchingGrant => { + ProtoTransactionEvalErrorKind::NoMatchingGrant(NoMatchingGrantError { + meaning: Some(meaning_to_proto(meaning)), + }) + } + PolicyError::Violations(violations) => { + ProtoTransactionEvalErrorKind::PolicyViolations(PolicyViolationsError { + meaning: Some(meaning_to_proto(meaning)), + violations: violations.into_iter().map(violation_to_proto).collect(), + }) + } + PolicyError::Pool(_) | PolicyError::Database(_) => { + return None; + } + }, + }; + + Some(TransactionEvalError { kind: Some(kind) }) +} + +fn decode_eip1559_transaction(payload: &[u8]) -> Result { + let mut body = payload; + if let Some((prefix, rest)) = payload.split_first() + && *prefix == 0x02 + { + body = rest; + } + + let mut cursor = body; + let transaction = TxEip1559::decode(&mut cursor).map_err(|_| ())?; + if !cursor.is_empty() { + return Err(()); + } + + Ok(transaction) +} + async fn dispatch_loop( mut bi: GrpcBi, actor: ActorRef, @@ -90,6 +210,64 @@ async fn dispatch_conn_message( } .into(), ), + ClientRequestPayload::EvmSignTransaction(request) => { + let wallet_address = match <[u8; 20]>::try_from(request.wallet_address.as_slice()) { + Ok(address) => Address::from(address), + Err(_) => { + let _ = bi + .send(Err(Status::invalid_argument("Invalid EVM wallet address"))) + .await; + return Err(()); + } + }; + + let transaction = match decode_eip1559_transaction(&request.rlp_transaction) { + Ok(transaction) => transaction, + Err(()) => { + let _ = bi + .send(Err(Status::invalid_argument( + "Invalid EIP-1559 RLP transaction", + ))) + .await; + return Err(()); + } + }; + + let response = match actor + .ask(HandleSignTransaction { + wallet_address, + transaction, + }) + .await + { + Ok(signature) => EvmSignTransactionResponse { + result: Some(EvmSignTransactionResult::Signature(signature.as_bytes().to_vec())), + }, + Err(kameo::error::SendError::HandlerError(SignTransactionRpcError::Vet(vet_error))) => { + match eval_error_to_proto(vet_error) { + Some(eval_error) => EvmSignTransactionResponse { + result: Some(EvmSignTransactionResult::EvalError(eval_error)), + }, + None => EvmSignTransactionResponse { + result: Some(EvmSignTransactionResult::Error(ProtoEvmError::Internal.into())), + }, + } + } + 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())), + } + } + }; + + ClientResponsePayload::EvmSignTransaction(response) + } payload => { warn!(?payload, "Unsupported post-auth client request"); let _ = bi diff --git a/server/crates/arbiter-server/src/grpc/client/auth.rs b/server/crates/arbiter-server/src/grpc/client/auth.rs index 49d8d55..8427efe 100644 --- a/server/crates/arbiter-server/src/grpc/client/auth.rs +++ b/server/crates/arbiter-server/src/grpc/client/auth.rs @@ -151,7 +151,9 @@ impl Receiver for AuthTransportAdapter<'_> { _ => { let _ = self .bi - .send(Err(Status::invalid_argument("Unsupported client auth request"))) + .send(Err(Status::invalid_argument( + "Unsupported client auth request", + ))) .await; None } @@ -168,6 +170,7 @@ pub async fn start( response_id: &mut Option, ) -> Result<(), auth::Error> { let mut transport = AuthTransportAdapter::new(bi, request_tracker, response_id); - client::auth::authenticate(conn, &mut transport).await?; + let authenticated = client::auth::authenticate(conn, &mut transport).await?; + conn.client_id = authenticated.client_id; Ok(()) } diff --git a/server/crates/arbiter-server/src/grpc/user_agent.rs b/server/crates/arbiter-server/src/grpc/user_agent.rs index 674471c..81cc863 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent.rs @@ -4,17 +4,24 @@ use arbiter_proto::{ google::protobuf::{Empty as ProtoEmpty, Timestamp as ProtoTimestamp}, proto::{ evm::{ - EtherTransferSettings as ProtoEtherTransferSettings, EvmError as ProtoEvmError, - EvmGrantCreateRequest, EvmGrantCreateResponse, EvmGrantDeleteRequest, - EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, GrantEntry, + EtherTransferSettings as ProtoEtherTransferSettings, + EvalViolation as ProtoEvalViolation, EvmError as ProtoEvmError, EvmGrantCreateRequest, + EvmGrantCreateResponse, EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, + EvmGrantListResponse, EvmSignTransactionResponse, GasLimitExceededViolation, + GrantEntry, NoMatchingGrantError, PolicyViolationsError, SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant, - TokenTransferSettings as ProtoTokenTransferSettings, + SpecificMeaning as ProtoSpecificMeaning, TokenInfo as ProtoTokenInfo, + TokenTransferSettings as ProtoTokenTransferSettings, TransactionEvalError, TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit, WalletCreateResponse, WalletEntry, WalletList, - WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult, + WalletListResponse, eval_violation::Kind as ProtoEvalViolationKind, + 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, specific_grant::Grant as ProtoSpecificGrantType, + specific_meaning::Meaning as ProtoSpecificMeaningKind, + transaction_eval_error::Kind as ProtoTransactionEvalErrorKind, wallet_create_response::Result as WalletCreateResult, wallet_list_response::Result as WalletListResult, }, @@ -23,8 +30,8 @@ use arbiter_proto::{ BootstrapResult as ProtoBootstrapResult, SdkClientConnectionResponse as ProtoSdkClientConnectionResponse, UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult, - UnsealStart, UserAgentRequest, UserAgentResponse, VaultState as ProtoVaultState, - user_agent_request::Payload as UserAgentRequestPayload, + UnsealStart, UserAgentEvmSignTransactionRequest, UserAgentRequest, UserAgentResponse, + VaultState as ProtoVaultState, user_agent_request::Payload as UserAgentRequestPayload, user_agent_response::Payload as UserAgentResponsePayload, }, }, @@ -47,7 +54,9 @@ use crate::{ session::{ BootstrapError, Error, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantList, - HandleQueryVaultState, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, + HandleQueryVaultState, HandleSignTransaction, HandleUnsealEncryptedKey, + HandleUnsealRequest, SignTransactionError as SessionSignTransactionError, + UnsealError, }, }, }, @@ -55,12 +64,124 @@ use crate::{ Grant, SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, ether_transfer, token_transfers, }, + evm::{PolicyError, VetError, policies::EvalViolation}, grpc::request_tracker::RequestTracker, utils::defer, }; -use alloy::primitives::{Address, U256}; +use alloy::{ + consensus::TxEip1559, + primitives::{Address, U256}, + rlp::Decodable, +}; mod auth; +fn u256_to_proto_bytes(value: U256) -> Vec { + value.to_be_bytes::<32>().to_vec() +} + +fn meaning_to_proto(meaning: crate::evm::policies::SpecificMeaning) -> ProtoSpecificMeaning { + let kind = match meaning { + crate::evm::policies::SpecificMeaning::EtherTransfer(meaning) => { + ProtoSpecificMeaningKind::EtherTransfer( + arbiter_proto::proto::evm::EtherTransferMeaning { + to: meaning.to.to_vec(), + value: u256_to_proto_bytes(meaning.value), + }, + ) + } + crate::evm::policies::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), + } +} + +fn violation_to_proto(violation: EvalViolation) -> ProtoEvalViolation { + let kind = match violation { + 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(ProtoEmpty {}) + } + EvalViolation::VolumetricLimitExceeded => { + ProtoEvalViolationKind::VolumetricLimitExceeded(ProtoEmpty {}) + } + EvalViolation::InvalidTime => ProtoEvalViolationKind::InvalidTime(ProtoEmpty {}), + EvalViolation::InvalidTransactionType => { + ProtoEvalViolationKind::InvalidTransactionType(ProtoEmpty {}) + } + }; + + ProtoEvalViolation { kind: Some(kind) } +} + +fn eval_error_to_proto(err: VetError) -> Option { + let kind = match err { + VetError::ContractCreationNotSupported => { + ProtoTransactionEvalErrorKind::ContractCreationNotSupported(ProtoEmpty {}) + } + VetError::UnsupportedTransactionType => { + ProtoTransactionEvalErrorKind::UnsupportedTransactionType(ProtoEmpty {}) + } + VetError::Evaluated(meaning, policy_error) => match policy_error { + PolicyError::NoMatchingGrant => { + ProtoTransactionEvalErrorKind::NoMatchingGrant(NoMatchingGrantError { + meaning: Some(meaning_to_proto(meaning)), + }) + } + PolicyError::Violations(violations) => { + ProtoTransactionEvalErrorKind::PolicyViolations(PolicyViolationsError { + meaning: Some(meaning_to_proto(meaning)), + violations: violations.into_iter().map(violation_to_proto).collect(), + }) + } + PolicyError::Pool(_) | PolicyError::Database(_) => { + return None; + } + }, + }; + + Some(TransactionEvalError { kind: Some(kind) }) +} + +fn decode_eip1559_transaction(payload: &[u8]) -> Result { + let mut body = payload; + if let Some((prefix, rest)) = payload.split_first() + && *prefix == 0x02 + { + body = rest; + } + + let mut cursor = body; + let transaction = TxEip1559::decode(&mut cursor).map_err(|_| ())?; + if !cursor.is_empty() { + return Err(()); + } + + Ok(transaction) +} + pub struct OutOfBandAdapter(mpsc::Sender); #[async_trait] @@ -271,6 +392,92 @@ async fn dispatch_conn_message( actor.ask(HandleGrantDelete { grant_id }).await, )) } + UserAgentRequestPayload::EvmSignTransaction(UserAgentEvmSignTransactionRequest { + client_id, + request, + }) => { + if client_id <= 0 { + let _ = bi + .send(Err(Status::invalid_argument("Invalid SDK client id"))) + .await; + return Err(()); + } + + let Some(request) = request else { + let _ = bi + .send(Err(Status::invalid_argument( + "Missing EVM sign transaction payload", + ))) + .await; + return Err(()); + }; + + let wallet_address = match <[u8; 20]>::try_from(request.wallet_address.as_slice()) { + Ok(address) => Address::from(address), + Err(_) => { + let _ = bi + .send(Err(Status::invalid_argument("Invalid EVM wallet address"))) + .await; + return Err(()); + } + }; + + let transaction = match decode_eip1559_transaction(&request.rlp_transaction) { + Ok(transaction) => transaction, + Err(()) => { + let _ = bi + .send(Err(Status::invalid_argument( + "Invalid EIP-1559 RLP transaction", + ))) + .await; + return Err(()); + } + }; + + let response = match actor + .ask(HandleSignTransaction { + client_id, + wallet_address, + transaction, + }) + .await + { + Ok(signature) => EvmSignTransactionResponse { + result: Some(EvmSignTransactionResult::Signature( + signature.as_bytes().to_vec(), + )), + }, + Err(SendError::HandlerError(SessionSignTransactionError::Vet(vet_error))) => { + match eval_error_to_proto(vet_error) { + Some(eval_error) => EvmSignTransactionResponse { + result: Some(EvmSignTransactionResult::EvalError(eval_error)), + }, + None => EvmSignTransactionResponse { + result: Some(EvmSignTransactionResult::Error( + ProtoEvmError::Internal.into(), + )), + }, + } + } + 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) + } payload => { warn!(?payload, "Unsupported post-auth user agent request"); let _ = bi -- 2.49.1