Merge pull request 'feat(evm): implement EVM sign transaction handling in client and user agent' (#38) from feat--self-signed-transactions into main
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
ci/woodpecker/push/useragent-analyze Pipeline failed

Reviewed-on: #38
Reviewed-by: Stas <business@jexter.tech>
This commit was merged in pull request #38.
This commit is contained in:
2026-04-02 19:26:05 +00:00
18 changed files with 504 additions and 78 deletions

View File

@@ -49,6 +49,7 @@ message ClientRequest {
AuthChallengeRequest auth_challenge_request = 1; AuthChallengeRequest auth_challenge_request = 1;
AuthChallengeSolution auth_challenge_solution = 2; AuthChallengeSolution auth_challenge_solution = 2;
google.protobuf.Empty query_vault_state = 3; google.protobuf.Empty query_vault_state = 3;
arbiter.evm.EvmSignTransactionRequest evm_sign_transaction = 5;
} }
} }

View File

@@ -154,6 +154,11 @@ message ListWalletAccessResponse {
repeated SdkClientWalletAccess accesses = 1; repeated SdkClientWalletAccess accesses = 1;
} }
message UserAgentEvmSignTransactionRequest {
int32 client_id = 1;
arbiter.evm.EvmSignTransactionRequest request = 2;
}
message UserAgentRequest { message UserAgentRequest {
int32 id = 16; int32 id = 16;
oneof payload { oneof payload {
@@ -174,6 +179,7 @@ message UserAgentRequest {
SdkClientGrantWalletAccess grant_wallet_access = 15; SdkClientGrantWalletAccess grant_wallet_access = 15;
SdkClientRevokeWalletAccess revoke_wallet_access = 17; SdkClientRevokeWalletAccess revoke_wallet_access = 17;
google.protobuf.Empty list_wallet_access = 18; google.protobuf.Empty list_wallet_access = 18;
UserAgentEvmSignTransactionRequest evm_sign_transaction = 19;
} }
} }
message UserAgentResponse { message UserAgentResponse {
@@ -195,5 +201,6 @@ message UserAgentResponse {
SdkClientListResponse sdk_client_list_response = 14; SdkClientListResponse sdk_client_list_response = 14;
BootstrapResult bootstrap_result = 15; BootstrapResult bootstrap_result = 15;
ListWalletAccessResponse list_wallet_access_response = 17; ListWalletAccessResponse list_wallet_access_response = 17;
arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 18;
} }
} }

View File

@@ -1,6 +1,4 @@
use arbiter_proto::proto::{ use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
client::{ClientRequest, ClientResponse},
};
use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::atomic::{AtomicI32, Ordering};
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -36,9 +34,7 @@ impl ClientTransport {
.map_err(|_| ClientSignError::ChannelClosed) .map_err(|_| ClientSignError::ChannelClosed)
} }
pub(crate) async fn recv( pub(crate) async fn recv(&mut self) -> std::result::Result<ClientResponse, ClientSignError> {
&mut self,
) -> std::result::Result<ClientResponse, ClientSignError> {
match self.receiver.message().await { match self.receiver.message().await {
Ok(Some(resp)) => Ok(resp), Ok(Some(resp)) => Ok(resp),
Ok(None) => Err(ClientSignError::ConnectionClosed), Ok(None) => Err(ClientSignError::ConnectionClosed),

View File

@@ -8,7 +8,15 @@ use async_trait::async_trait;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; 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 { pub struct ArbiterEvmWallet {
transport: Arc<Mutex<ClientTransport>>, transport: Arc<Mutex<ClientTransport>>,
@@ -79,11 +87,61 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
&self, &self,
tx: &mut dyn SignableTransaction<Signature>, tx: &mut dyn SignableTransaction<Signature>,
) -> Result<Signature> { ) -> Result<Signature> {
let _transport = self.transport.lock().await;
self.validate_chain_id(tx)?; self.validate_chain_id(tx)?;
Err(Error::other( let mut transport = self.transport.lock().await;
"transaction signing is not supported by current arbiter.client protocol", 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}"
))),
}
} }
} }

View File

@@ -1,5 +1,6 @@
use arbiter_proto::{ use arbiter_proto::{
ClientMetadata, format_challenge, transport::{Bi, expect_message} ClientMetadata, format_challenge,
transport::{Bi, expect_message},
}; };
use chrono::Utc; use chrono::Utc;
use diesel::{ use diesel::{
@@ -83,7 +84,6 @@ async fn get_client_and_nonce(
})?; })?;
conn.exclusive_transaction(|conn| { conn.exclusive_transaction(|conn| {
let pubkey_bytes = pubkey_bytes.clone();
Box::pin(async move { Box::pin(async move {
let Some((client_id, current_nonce)) = program_client::table let Some((client_id, current_nonce)) = program_client::table
.filter(program_client::public_key.eq(&pubkey_bytes)) .filter(program_client::public_key.eq(&pubkey_bytes))
@@ -290,7 +290,7 @@ where
pub async fn authenticate<T>( pub async fn authenticate<T>(
props: &mut ClientConnection, props: &mut ClientConnection,
transport: &mut T, transport: &mut T,
) -> Result<VerifyingKey, Error> ) -> Result<i32, Error>
where where
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized, T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
{ {
@@ -318,7 +318,6 @@ where
}; };
sync_client_metadata(&props.db, info.id, &metadata).await?; sync_client_metadata(&props.db, info.id, &metadata).await?;
challenge_client(transport, pubkey, info.current_nonce).await?; challenge_client(transport, pubkey, info.current_nonce).await?;
transport transport
@@ -329,5 +328,5 @@ where
Error::Transport Error::Transport
})?; })?;
Ok(pubkey) Ok(info.id)
} }

View File

@@ -3,7 +3,7 @@ use kameo::actor::Spawn;
use tracing::{error, info}; use tracing::{error, info};
use crate::{ use crate::{
actors::{GlobalActors, client::{ session::ClientSession}}, actors::{GlobalActors, client::session::ClientSession},
db, db,
}; };
@@ -20,7 +20,10 @@ pub struct ClientConnection {
impl ClientConnection { impl ClientConnection {
pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self { pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self {
Self { db, actors } Self {
db,
actors,
}
} }
} }
@@ -32,8 +35,8 @@ where
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send + ?Sized, T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send + ?Sized,
{ {
match auth::authenticate(&mut props, transport).await { match auth::authenticate(&mut props, transport).await {
Ok(_pubkey) => { Ok(client_id) => {
ClientSession::spawn(ClientSession::new(props)); ClientSession::spawn(ClientSession::new(props, client_id));
info!("Client authenticated, session started"); info!("Client authenticated, session started");
} }
Err(err) => { Err(err) => {

View File

@@ -1,21 +1,30 @@
use ed25519_dalek::VerifyingKey;
use kameo::{Actor, messages}; use kameo::{Actor, messages};
use tracing::error; use tracing::error;
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
use crate::{ use crate::{
actors::{ actors::{
GlobalActors, client::ClientConnection, flow_coordinator::RegisterClient, GlobalActors,
client::ClientConnection, flow_coordinator::RegisterClient,
evm::{ClientSignTransaction, SignTransactionError},
keyholder::KeyHolderState, keyholder::KeyHolderState,
}, },
db, db,
evm::VetError,
}; };
pub struct ClientSession { pub struct ClientSession {
props: ClientConnection, props: ClientConnection,
client_id: i32,
} }
impl ClientSession { impl ClientSession {
pub(crate) fn new(props: ClientConnection) -> Self { pub(crate) fn new(props: ClientConnection, client_id: i32) -> Self {
Self { props } Self { props, client_id }
} }
} }
@@ -35,6 +44,34 @@ impl ClientSession {
Ok(vault_state) Ok(vault_state)
} }
#[message]
pub(crate) async fn handle_sign_transaction(
&mut self,
wallet_address: Address,
transaction: TxEip1559,
) -> Result<Signature, SignTransactionRpcError> {
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 { impl Actor for ClientSession {
@@ -59,7 +96,7 @@ impl Actor for ClientSession {
impl ClientSession { impl ClientSession {
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self { pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
let props = ClientConnection::new(db, actors); let props = ClientConnection::new(db, actors);
Self { props } Self { props, client_id: 0 }
} }
} }
@@ -70,3 +107,12 @@ pub enum Error {
#[error("Internal error")] #[error("Internal error")]
Internal, Internal,
} }
#[derive(Debug, thiserror::Error)]
pub enum SignTransactionRpcError {
#[error("Policy evaluation failed")]
Vet(#[from] VetError),
#[error("Internal error")]
Internal,
}

View File

@@ -1,6 +1,6 @@
use std::sync::Mutex; use std::sync::Mutex;
use alloy::primitives::Address; use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use diesel::sql_types::ops::Add; use diesel::sql_types::ops::Add;
use diesel::{BoolExpressionMethods as _, ExpressionMethods as _, QueryDsl as _, SelectableHelper}; use diesel::{BoolExpressionMethods as _, ExpressionMethods as _, QueryDsl as _, SelectableHelper};
@@ -23,7 +23,8 @@ use crate::safe_cell::SafeCell;
use crate::{ use crate::{
actors::{ actors::{
evm::{ evm::{
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants, ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
}, },
keyholder::{self, Bootstrap, TryUnseal}, keyholder::{self, Bootstrap, TryUnseal},
user_agent::session::{ user_agent::session::{
@@ -112,6 +113,15 @@ pub enum BootstrapError {
General(#[from] super::Error), 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] #[messages]
impl UserAgentSession { impl UserAgentSession {
#[message] #[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<Signature, SignTransactionError> {
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] #[message]
pub(crate) async fn handle_grant_evm_wallet_access( pub(crate) async fn handle_grant_evm_wallet_access(
&mut self, &mut self,

View File

@@ -32,7 +32,7 @@ mod utils;
#[derive(Debug, thiserror::Error, miette::Diagnostic)] #[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum PolicyError { pub enum PolicyError {
#[error("Database error")] #[error("Database error")]
Error(#[from] crate::db::DatabaseError), Database(#[from] crate::db::DatabaseError),
#[error("Transaction violates policy: {0:?}")] #[error("Transaction violates policy: {0:?}")]
#[diagnostic(code(arbiter_server::evm::policy_error::violation))] #[diagnostic(code(arbiter_server::evm::policy_error::violation))]
Violations(Vec<EvalViolation>), Violations(Vec<EvalViolation>),

View File

@@ -36,8 +36,8 @@ use super::{DatabaseID, EvalContext, EvalViolation};
// Plain ether transfer // Plain ether transfer
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Meaning { pub struct Meaning {
to: Address, pub(crate) to: Address,
value: U256, pub(crate) value: U256,
} }
impl Display for Meaning { impl Display for Meaning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View File

@@ -38,9 +38,9 @@ fn grant_join() -> _ {
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Meaning { pub struct Meaning {
token: &'static TokenInfo, pub(crate) token: &'static TokenInfo,
to: Address, pub(crate) to: Address,
value: U256, pub(crate) value: U256,
} }
impl std::fmt::Display for Meaning { impl std::fmt::Display for Meaning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View File

@@ -1,8 +1,15 @@
use alloy::primitives::Address;
use arbiter_proto::{ use arbiter_proto::{
proto::client::{ proto::{
ClientRequest, ClientResponse, VaultState as ProtoVaultState, client::{
client_request::Payload as ClientRequestPayload, ClientRequest, ClientResponse, VaultState as ProtoVaultState,
client_response::Payload as ClientResponsePayload, 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}, transport::{Receiver, Sender, grpc::GrpcBi},
}; };
@@ -17,11 +24,18 @@ use crate::{
actors::{ actors::{
client::{ client::{
self, ClientConnection, self, ClientConnection,
session::{ClientSession, Error, HandleQueryVaultState}, session::{
ClientSession, Error, HandleQueryVaultState, HandleSignTransaction,
SignTransactionRpcError,
},
}, },
keyholder::KeyHolderState, keyholder::KeyHolderState,
}, },
grpc::request_tracker::RequestTracker, grpc::{
Convert, TryConvert,
common::inbound::{RawEvmAddress, RawEvmTransaction},
request_tracker::RequestTracker,
},
}; };
mod auth; mod auth;
@@ -34,7 +48,9 @@ async fn dispatch_loop(
mut request_tracker: RequestTracker, mut request_tracker: RequestTracker,
) { ) {
loop { loop {
let Some(message) = bi.recv().await else { return }; let Some(message) = bi.recv().await else {
return;
};
let conn = match message { let conn = match message {
Ok(conn) => conn, Ok(conn) => conn,
@@ -53,16 +69,24 @@ async fn dispatch_loop(
}; };
let Some(payload) = conn.payload else { 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; return;
}; };
match dispatch_inner(&actor, payload).await { match dispatch_inner(&actor, payload).await {
Ok(response) => { Ok(response) => {
if bi.send(Ok(ClientResponse { if bi
request_id: Some(request_id), .send(Ok(ClientResponse {
payload: Some(response), request_id: Some(request_id),
})).await.is_err() { payload: Some(response),
}))
.await
.is_err()
{
return; return;
} }
} }
@@ -92,6 +116,47 @@ async fn dispatch_inner(
}; };
Ok(ClientResponsePayload::VaultState(state.into())) 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 => { payload => {
warn!(?payload, "Unsupported post-auth client request"); warn!(?payload, "Unsupported post-auth client request");
Err(Status::invalid_argument("Unsupported 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<ClientRequest, ClientResponse>) { pub async fn start(mut conn: ClientConnection, mut bi: GrpcBi<ClientRequest, ClientResponse>) {
let mut request_tracker = RequestTracker::default(); let mut request_tracker = RequestTracker::default();
if let Err(e) = auth::start(&mut conn, &mut bi, &mut request_tracker).await { let client_id = match auth::start(&mut conn, &mut bi, &mut request_tracker).await {
let mut transport = auth::AuthTransportAdapter::new(&mut bi, &mut request_tracker); Ok(id) => id,
let _ = transport.send(Err(e.clone())).await; Err(err) => {
warn!(error = ?e, "Client authentication failed"); let _ = bi
return; .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(); let actor_for_cleanup = actor.clone();
info!("Client authenticated successfully"); info!("Client authenticated successfully");

View File

@@ -1,11 +1,13 @@
use arbiter_proto::{ use arbiter_proto::{
ClientMetadata, proto::client::{ ClientMetadata,
proto::client::{
AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest, AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest,
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult, AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
ClientInfo as ProtoClientInfo, ClientRequest, ClientResponse, ClientInfo as ProtoClientInfo, ClientRequest, ClientResponse,
client_request::Payload as ClientRequestPayload, client_request::Payload as ClientRequestPayload,
client_response::Payload as ClientResponsePayload, 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 async_trait::async_trait;
use tonic::Status; use tonic::Status;
@@ -181,8 +183,7 @@ pub async fn start(
conn: &mut ClientConnection, conn: &mut ClientConnection,
bi: &mut GrpcBi<ClientRequest, ClientResponse>, bi: &mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &mut RequestTracker, request_tracker: &mut RequestTracker,
) -> Result<(), auth::Error> { ) -> Result<i32, auth::Error> {
let mut transport = AuthTransportAdapter::new(bi, request_tracker); let mut transport = AuthTransportAdapter::new(bi, request_tracker);
client::auth::authenticate(conn, &mut transport).await?; client::auth::authenticate(conn, &mut transport).await
Ok(())
} }

View File

@@ -0,0 +1,2 @@
pub mod inbound;
pub mod outbound;

View File

@@ -0,0 +1,36 @@
use alloy::{consensus::TxEip1559, primitives::Address, rlp::Decodable as _};
use crate::grpc::TryConvert;
pub struct RawEvmAddress(pub Vec<u8>);
impl TryConvert for RawEvmAddress {
type Output = Address;
type Error = tonic::Status;
fn try_convert(self) -> Result<Self::Output, Self::Error> {
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<u8>);
impl TryConvert for RawEvmTransaction {
type Output = TxEip1559;
type Error = tonic::Status;
fn try_convert(mut self) -> Result<Self::Output, Self::Error> {
let tx = TxEip1559::decode(&mut self.0.as_slice()).map_err(|_| {
tonic::Status::invalid_argument("Invalid EVM transaction format")
})?;
Ok(tx)
}
}

View File

@@ -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<u8> {
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())
}
}

View File

@@ -14,10 +14,13 @@ use crate::{
grpc::user_agent::start, grpc::user_agent::start,
}; };
pub mod client;
mod request_tracker; mod request_tracker;
pub mod client;
pub mod user_agent; pub mod user_agent;
mod common;
pub trait Convert { pub trait Convert {
type Output; type Output;

View File

@@ -6,10 +6,11 @@ use arbiter_proto::{
evm::{ evm::{
EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse, EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse,
EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse,
GrantEntry, WalletCreateResponse, WalletEntry, WalletList, WalletListResponse, EvmSignTransactionResponse, GrantEntry, WalletCreateResponse, WalletEntry, WalletList,
evm_grant_create_response::Result as EvmGrantCreateResult, WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult,
evm_grant_delete_response::Result as EvmGrantDeleteResult, evm_grant_delete_response::Result as EvmGrantDeleteResult,
evm_grant_list_response::Result as EvmGrantListResult, evm_grant_list_response::Result as EvmGrantListResult,
evm_sign_transaction_response::Result as EvmSignTransactionResult,
wallet_create_response::Result as WalletCreateResult, wallet_create_response::Result as WalletCreateResult,
wallet_list_response::Result as WalletListResult, wallet_list_response::Result as WalletListResult,
}, },
@@ -22,8 +23,8 @@ use arbiter_proto::{
SdkClientGrantWalletAccess, SdkClientList as ProtoSdkClientList, SdkClientGrantWalletAccess, SdkClientList as ProtoSdkClientList,
SdkClientListResponse as ProtoSdkClientListResponse, SdkClientRevokeWalletAccess, SdkClientListResponse as ProtoSdkClientListResponse, SdkClientRevokeWalletAccess,
SdkClientWalletAccess, UnsealEncryptedKey as ProtoUnsealEncryptedKey, SdkClientWalletAccess, UnsealEncryptedKey as ProtoUnsealEncryptedKey,
UnsealResult as ProtoUnsealResult, UnsealStart, UserAgentRequest, UserAgentResponse, UnsealResult as ProtoUnsealResult, UnsealStart, UserAgentEvmSignTransactionRequest,
VaultState as ProtoVaultState, UserAgentRequest, UserAgentResponse, VaultState as ProtoVaultState,
sdk_client_list_response::Result as ProtoSdkClientListResult, sdk_client_list_response::Result as ProtoSdkClientListResult,
user_agent_request::Payload as UserAgentRequestPayload, user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload, user_agent_response::Payload as UserAgentResponsePayload,
@@ -49,12 +50,24 @@ use crate::{
HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete,
HandleGrantEvmWalletAccess, HandleGrantList, HandleListWalletAccess, HandleGrantEvmWalletAccess, HandleGrantList, HandleListWalletAccess,
HandleNewClientApprove, HandleQueryVaultState, HandleRevokeEvmWalletAccess, HandleNewClientApprove, HandleQueryVaultState, HandleRevokeEvmWalletAccess,
HandleSdkClientList, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, HandleSdkClientList, HandleSignTransaction, HandleUnsealEncryptedKey,
HandleUnsealRequest, SignTransactionError as SessionSignTransactionError,
UnsealError,
}, },
}, },
}, },
db::models::{CoreEvmWalletAccess, NewEvmWalletAccess}, 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 auth;
mod inbound; mod inbound;
@@ -178,7 +191,6 @@ async fn dispatch_inner(
}, },
) )
} }
UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey { UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey {
nonce, nonce,
ciphertext, ciphertext,
@@ -203,7 +215,6 @@ async fn dispatch_inner(
}; };
UserAgentResponsePayload::UnsealResult(result.into()) UserAgentResponsePayload::UnsealResult(result.into())
} }
UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey { UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey {
nonce, nonce,
ciphertext, ciphertext,
@@ -231,7 +242,6 @@ async fn dispatch_inner(
}; };
UserAgentResponsePayload::BootstrapResult(result.into()) UserAgentResponsePayload::BootstrapResult(result.into())
} }
UserAgentRequestPayload::QueryVaultState(_) => { UserAgentRequestPayload::QueryVaultState(_) => {
let state = match actor.ask(HandleQueryVaultState {}).await { let state = match actor.ask(HandleQueryVaultState {}).await {
Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
@@ -244,7 +254,6 @@ async fn dispatch_inner(
}; };
UserAgentResponsePayload::VaultState(state.into()) UserAgentResponsePayload::VaultState(state.into())
} }
UserAgentRequestPayload::EvmWalletCreate(_) => { UserAgentRequestPayload::EvmWalletCreate(_) => {
let result = match actor.ask(HandleEvmWalletCreate {}).await { let result = match actor.ask(HandleEvmWalletCreate {}).await {
Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry { Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry {
@@ -260,7 +269,6 @@ async fn dispatch_inner(
result: Some(result), result: Some(result),
}) })
} }
UserAgentRequestPayload::EvmWalletList(_) => { UserAgentRequestPayload::EvmWalletList(_) => {
let result = match actor.ask(HandleEvmWalletList {}).await { let result = match actor.ask(HandleEvmWalletList {}).await {
Ok(wallets) => WalletListResult::Wallets(WalletList { Ok(wallets) => WalletListResult::Wallets(WalletList {
@@ -281,7 +289,6 @@ async fn dispatch_inner(
result: Some(result), result: Some(result),
}) })
} }
UserAgentRequestPayload::EvmGrantList(_) => { UserAgentRequestPayload::EvmGrantList(_) => {
let result = match actor.ask(HandleGrantList {}).await { let result = match actor.ask(HandleGrantList {}).await {
Ok(grants) => EvmGrantListResult::Grants(EvmGrantList { Ok(grants) => EvmGrantListResult::Grants(EvmGrantList {
@@ -304,7 +311,6 @@ async fn dispatch_inner(
result: Some(result), result: Some(result),
}) })
} }
UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest { shared, specific }) => { UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest { shared, specific }) => {
let basic = shared let basic = shared
.ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))? .ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))?
@@ -324,7 +330,6 @@ async fn dispatch_inner(
result: Some(result), result: Some(result),
}) })
} }
UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id }) => { UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id }) => {
let result = match actor.ask(HandleGrantDelete { grant_id }).await { let result = match actor.ask(HandleGrantDelete { grant_id }).await {
Ok(()) => EvmGrantDeleteResult::Ok(()), Ok(()) => EvmGrantDeleteResult::Ok(()),
@@ -337,7 +342,6 @@ async fn dispatch_inner(
result: Some(result), result: Some(result),
}) })
} }
UserAgentRequestPayload::SdkClientConnectionResponse(resp) => { UserAgentRequestPayload::SdkClientConnectionResponse(resp) => {
let pubkey_bytes = <[u8; 32]>::try_from(resp.pubkey) let pubkey_bytes = <[u8; 32]>::try_from(resp.pubkey)
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key length"))?; .map_err(|_| Status::invalid_argument("Invalid Ed25519 public key length"))?;
@@ -357,9 +361,7 @@ async fn dispatch_inner(
return Ok(None); return Ok(None);
} }
UserAgentRequestPayload::SdkClientRevoke(_) => todo!(), UserAgentRequestPayload::SdkClientRevoke(_) => todo!(),
UserAgentRequestPayload::SdkClientList(_) => { UserAgentRequestPayload::SdkClientList(_) => {
let result = match actor.ask(HandleSdkClientList {}).await { let result = match actor.ask(HandleSdkClientList {}).await {
Ok(clients) => ProtoSdkClientListResult::Clients(ProtoSdkClientList { Ok(clients) => ProtoSdkClientListResult::Clients(ProtoSdkClientList {
@@ -386,7 +388,6 @@ async fn dispatch_inner(
result: Some(result), result: Some(result),
}) })
} }
UserAgentRequestPayload::GrantWalletAccess(SdkClientGrantWalletAccess { accesses }) => { UserAgentRequestPayload::GrantWalletAccess(SdkClientGrantWalletAccess { accesses }) => {
let entries: Vec<NewEvmWalletAccess> = let entries: Vec<NewEvmWalletAccess> =
accesses.into_iter().map(|a| a.convert()).collect(); accesses.into_iter().map(|a| a.convert()).collect();
@@ -402,9 +403,11 @@ async fn dispatch_inner(
} }
} }
} }
UserAgentRequestPayload::RevokeWalletAccess(SdkClientRevokeWalletAccess { accesses }) => { UserAgentRequestPayload::RevokeWalletAccess(SdkClientRevokeWalletAccess { accesses }) => {
match actor.ask(HandleRevokeEvmWalletAccess { entries: accesses }).await { match actor
.ask(HandleRevokeEvmWalletAccess { entries: accesses })
.await
{
Ok(()) => { Ok(()) => {
info!("Successfully revoked wallet access"); info!("Successfully revoked wallet access");
return Ok(None); return Ok(None);
@@ -415,7 +418,6 @@ async fn dispatch_inner(
} }
} }
} }
UserAgentRequestPayload::ListWalletAccess(_) => { UserAgentRequestPayload::ListWalletAccess(_) => {
let result = match actor.ask(HandleListWalletAccess {}).await { let result = match actor.ask(HandleListWalletAccess {}).await {
Ok(accesses) => ListWalletAccessResponse { Ok(accesses) => ListWalletAccessResponse {
@@ -428,12 +430,59 @@ async fn dispatch_inner(
}; };
UserAgentResponsePayload::ListWalletAccessResponse(result) UserAgentResponsePayload::ListWalletAccessResponse(result)
} }
UserAgentRequestPayload::AuthChallengeRequest(..) UserAgentRequestPayload::AuthChallengeRequest(..)
| UserAgentRequestPayload::AuthChallengeSolution(..) => { | UserAgentRequestPayload::AuthChallengeSolution(..) => {
warn!(?payload, "Unsupported post-auth user agent request"); warn!(?payload, "Unsupported post-auth user agent request");
return Err(Status::invalid_argument("Unsupported 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)) Ok(Some(response))