feat(evm): implement EVM sign transaction handling in client and user agent
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline was successful

This commit is contained in:
CleverWild
2026-03-26 19:57:48 +01:00
parent 2148faa376
commit 6987e5f70f
14 changed files with 605 additions and 51 deletions

View File

@@ -42,6 +42,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

@@ -137,6 +137,11 @@ message SdkClientConnectionResponse {
message SdkClientConnectionCancel {} message SdkClientConnectionCancel {}
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 {
@@ -155,6 +160,7 @@ message UserAgentRequest {
SdkClientRevokeRequest sdk_client_revoke = 13; SdkClientRevokeRequest sdk_client_revoke = 13;
google.protobuf.Empty sdk_client_list = 14; google.protobuf.Empty sdk_client_list = 14;
BootstrapEncryptedKey bootstrap_encrypted_key = 15; BootstrapEncryptedKey bootstrap_encrypted_key = 15;
UserAgentEvmSignTransactionRequest evm_sign_transaction = 17;
} }
} }
message UserAgentResponse { message UserAgentResponse {
@@ -175,5 +181,6 @@ message UserAgentResponse {
SdkClientRevokeResponse sdk_client_revoke_response = 13; SdkClientRevokeResponse sdk_client_revoke_response = 13;
SdkClientListResponse sdk_client_list_response = 14; SdkClientListResponse sdk_client_list_response = 14;
BootstrapResult bootstrap_result = 15; BootstrapResult bootstrap_result = 15;
arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 17;
} }
} }

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

@@ -54,10 +54,19 @@ pub enum Outbound {
AuthSuccess, AuthSuccess,
} }
#[derive(Debug, Clone)]
pub struct AuthenticatedClient {
pub pubkey: VerifyingKey,
pub client_id: i32,
}
/// Atomically reads and increments the nonce for a known client. /// Atomically reads and increments the nonce for a known client.
/// Returns `None` if the pubkey is not registered. /// Returns `None` if the pubkey is not registered.
async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Option<i32>, Error> { async fn get_nonce(
let pubkey_bytes = pubkey.as_bytes().to_vec(); db: &db::DatabasePool,
pubkey: &VerifyingKey,
) -> Result<Option<(/* client_id */ i32, /* nonce */ i32)>, Error> {
let pubkey_bytes = pubkey.as_bytes();
let mut conn = db.get().await.map_err(|e| { let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error"); error!(error = ?e, "Database pool error");
@@ -65,7 +74,6 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
})?; })?;
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))
@@ -83,8 +91,7 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
.execute(conn) .execute(conn)
.await?; .await?;
let _ = client_id; Ok(Some((client_id, current_nonce)))
Ok(Some(current_nonce))
}) })
}) })
.await .await
@@ -213,23 +220,25 @@ 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<AuthenticatedClient, Error>
where where
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized, T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
{ {
let Some(Inbound::AuthChallengeRequest { pubkey }) = transport.recv().await let Some(Inbound::AuthChallengeRequest { pubkey }) = transport.recv().await else {
else {
return Err(Error::Transport); return Err(Error::Transport);
}; };
let nonce = match get_nonce(&props.db, &pubkey).await? { let (client_id, nonce) = match get_nonce(&props.db, &pubkey).await? {
Some(nonce) => nonce, Some(client_nonce) => client_nonce,
None => { None => {
approve_new_client(&props.actors, pubkey).await?; approve_new_client(&props.actors, pubkey).await?;
match insert_client(&props.db, &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? { InsertClientResult::AlreadyExists => match get_nonce(&props.db, &pubkey).await? {
Some(nonce) => nonce, Some((client_id, nonce)) => (client_id, nonce),
None => return Err(Error::DatabaseOperationFailed), None => return Err(Error::DatabaseOperationFailed),
}, },
} }
@@ -245,5 +254,5 @@ where
Error::Transport Error::Transport
})?; })?;
Ok(pubkey) Ok(AuthenticatedClient { pubkey, client_id })
} }

View File

@@ -10,11 +10,16 @@ use crate::{
pub struct ClientConnection { pub struct ClientConnection {
pub(crate) db: db::DatabasePool, pub(crate) db: db::DatabasePool,
pub(crate) actors: GlobalActors, pub(crate) actors: GlobalActors,
pub(crate) client_id: i32,
} }
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,
client_id: 0,
}
} }
} }
@@ -26,7 +31,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(authenticated) => {
props.client_id = authenticated.client_id;
ClientSession::spawn(ClientSession::new(props)); ClientSession::spawn(ClientSession::new(props));
info!("Client authenticated, session started"); info!("Client authenticated, session started");
} }

View File

@@ -1,11 +1,18 @@
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, keyholder::KeyHolderState, router::RegisterClient, GlobalActors,
client::ClientConnection,
evm::{ClientSignTransaction, SignTransactionError},
keyholder::KeyHolderState,
router::RegisterClient,
}, },
db, db,
evm::VetError,
}; };
pub struct ClientSession { pub struct ClientSession {
@@ -34,6 +41,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.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 { impl Actor for ClientSession {
@@ -69,3 +104,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

@@ -36,7 +36,10 @@ impl Error {
pub struct UserAgentSession { pub struct UserAgentSession {
props: UserAgentConnection, props: UserAgentConnection,
state: UserAgentStateMachine<DummyContext>, state: UserAgentStateMachine<DummyContext>,
#[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<dyn Sender<OutOfBand>>, sender: Box<dyn Sender<OutOfBand>>,
} }
@@ -44,8 +47,11 @@ mod connection;
pub(crate) use connection::{ pub(crate) use connection::{
BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, HandleEvmWalletList, BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, HandleEvmWalletList,
HandleGrantCreate, HandleGrantDelete, HandleGrantList, HandleQueryVaultState, HandleGrantCreate, HandleGrantDelete, HandleGrantList, HandleQueryVaultState,
HandleSignTransaction,
};
pub use connection::{
HandleUnsealEncryptedKey, HandleUnsealRequest, SignTransactionError, UnsealError,
}; };
pub use connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError};
impl UserAgentSession { impl UserAgentSession {
pub(crate) fn new(props: UserAgentConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self { pub(crate) fn new(props: UserAgentConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self {

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 kameo::error::SendError; use kameo::error::SendError;
use kameo::messages; use kameo::messages;
@@ -14,13 +14,14 @@ 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::{
UserAgentSession, UserAgentSession,
state::{UnsealContext, UserAgentEvents, UserAgentStates}, state::{UnsealContext, UserAgentEvents, UserAgentStates},
}, },
}, },
safe_cell::SafeCellHandle as _, safe_cell::SafeCellHandle as _,
}; };
@@ -103,6 +104,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]
@@ -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<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)
}
}
}
} }

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,4 +1,5 @@
use arbiter_proto::{ use arbiter_proto::{
google::protobuf::Empty as ProtoEmpty,
proto::client::{ proto::client::{
ClientRequest, ClientResponse, VaultState as ProtoVaultState, ClientRequest, ClientResponse, VaultState as ProtoVaultState,
client_request::Payload as ClientRequestPayload, client_request::Payload as ClientRequestPayload,
@@ -17,16 +18,135 @@ use crate::{
actors::{ actors::{
client::{ client::{
self, ClientConnection, self, ClientConnection,
session::{ClientSession, Error, HandleQueryVaultState}, session::{
ClientSession, Error, HandleQueryVaultState, HandleSignTransaction,
SignTransactionRpcError,
},
}, },
keyholder::KeyHolderState, keyholder::KeyHolderState,
}, },
evm::{PolicyError, VetError, policies::EvalViolation},
grpc::request_tracker::RequestTracker, grpc::request_tracker::RequestTracker,
utils::defer, 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; mod auth;
fn u256_to_proto_bytes(value: U256) -> Vec<u8> {
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<TransactionEvalError> {
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<TxEip1559, ()> {
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( async fn dispatch_loop(
mut bi: GrpcBi<ClientRequest, ClientResponse>, mut bi: GrpcBi<ClientRequest, ClientResponse>,
actor: ActorRef<ClientSession>, actor: ActorRef<ClientSession>,
@@ -90,6 +210,64 @@ async fn dispatch_conn_message(
} }
.into(), .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 => { payload => {
warn!(?payload, "Unsupported post-auth client request"); warn!(?payload, "Unsupported post-auth client request");
let _ = bi let _ = bi

View File

@@ -151,7 +151,9 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
_ => { _ => {
let _ = self let _ = self
.bi .bi
.send(Err(Status::invalid_argument("Unsupported client auth request"))) .send(Err(Status::invalid_argument(
"Unsupported client auth request",
)))
.await; .await;
None None
} }
@@ -168,6 +170,7 @@ pub async fn start(
response_id: &mut Option<i32>, response_id: &mut Option<i32>,
) -> Result<(), auth::Error> { ) -> Result<(), auth::Error> {
let mut transport = AuthTransportAdapter::new(bi, request_tracker, response_id); 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(()) Ok(())
} }

View File

@@ -4,17 +4,24 @@ use arbiter_proto::{
google::protobuf::{Empty as ProtoEmpty, Timestamp as ProtoTimestamp}, google::protobuf::{Empty as ProtoEmpty, Timestamp as ProtoTimestamp},
proto::{ proto::{
evm::{ evm::{
EtherTransferSettings as ProtoEtherTransferSettings, EvmError as ProtoEvmError, EtherTransferSettings as ProtoEtherTransferSettings,
EvmGrantCreateRequest, EvmGrantCreateResponse, EvmGrantDeleteRequest, EvalViolation as ProtoEvalViolation, EvmError as ProtoEvmError, EvmGrantCreateRequest,
EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, GrantEntry, EvmGrantCreateResponse, EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList,
EvmGrantListResponse, EvmSignTransactionResponse, GasLimitExceededViolation,
GrantEntry, NoMatchingGrantError, PolicyViolationsError,
SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant, SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant,
TokenTransferSettings as ProtoTokenTransferSettings, SpecificMeaning as ProtoSpecificMeaning, TokenInfo as ProtoTokenInfo,
TokenTransferSettings as ProtoTokenTransferSettings, TransactionEvalError,
TransactionRateLimit as ProtoTransactionRateLimit, TransactionRateLimit as ProtoTransactionRateLimit,
VolumeRateLimit as ProtoVolumeRateLimit, WalletCreateResponse, WalletEntry, WalletList, 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_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,
specific_grant::Grant as ProtoSpecificGrantType, specific_grant::Grant as ProtoSpecificGrantType,
specific_meaning::Meaning as ProtoSpecificMeaningKind,
transaction_eval_error::Kind as ProtoTransactionEvalErrorKind,
wallet_create_response::Result as WalletCreateResult, wallet_create_response::Result as WalletCreateResult,
wallet_list_response::Result as WalletListResult, wallet_list_response::Result as WalletListResult,
}, },
@@ -23,8 +30,8 @@ use arbiter_proto::{
BootstrapResult as ProtoBootstrapResult, BootstrapResult as ProtoBootstrapResult,
SdkClientConnectionResponse as ProtoSdkClientConnectionResponse, SdkClientConnectionResponse as ProtoSdkClientConnectionResponse,
UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult, UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult,
UnsealStart, UserAgentRequest, UserAgentResponse, VaultState as ProtoVaultState, UnsealStart, UserAgentEvmSignTransactionRequest, UserAgentRequest, UserAgentResponse,
user_agent_request::Payload as UserAgentRequestPayload, VaultState as ProtoVaultState, user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload, user_agent_response::Payload as UserAgentResponsePayload,
}, },
}, },
@@ -47,7 +54,9 @@ use crate::{
session::{ session::{
BootstrapError, Error, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, BootstrapError, Error, HandleBootstrapEncryptedKey, HandleEvmWalletCreate,
HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantList, 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, Grant, SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit,
ether_transfer, token_transfers, ether_transfer, token_transfers,
}, },
evm::{PolicyError, VetError, policies::EvalViolation},
grpc::request_tracker::RequestTracker, grpc::request_tracker::RequestTracker,
utils::defer, utils::defer,
}; };
use alloy::primitives::{Address, U256}; use alloy::{
consensus::TxEip1559,
primitives::{Address, U256},
rlp::Decodable,
};
mod auth; mod auth;
fn u256_to_proto_bytes(value: U256) -> Vec<u8> {
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<TransactionEvalError> {
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<TxEip1559, ()> {
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<OutOfBand>); pub struct OutOfBandAdapter(mpsc::Sender<OutOfBand>);
#[async_trait] #[async_trait]
@@ -271,6 +392,92 @@ async fn dispatch_conn_message(
actor.ask(HandleGrantDelete { grant_id }).await, 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 => { payload => {
warn!(?payload, "Unsupported post-auth user agent request"); warn!(?payload, "Unsupported post-auth user agent request");
let _ = bi let _ = bi