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;
AuthChallengeSolution auth_challenge_solution = 2;
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 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;
}
}

View File

@@ -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<ClientResponse, ClientSignError> {
pub(crate) async fn recv(&mut self) -> std::result::Result<ClientResponse, ClientSignError> {
match self.receiver.message().await {
Ok(Some(resp)) => Ok(resp),
Ok(None) => Err(ClientSignError::ConnectionClosed),

View File

@@ -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<Mutex<ClientTransport>>,
@@ -79,11 +87,61 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> Result<Signature> {
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}"
))),
}
}
}

View File

@@ -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<Option<i32>, Error> {
let pubkey_bytes = pubkey.as_bytes().to_vec();
async fn get_nonce(
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| {
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| {
let pubkey_bytes = pubkey_bytes.clone();
Box::pin(async move {
let Some((client_id, current_nonce)) = program_client::table
.filter(program_client::public_key.eq(&pubkey_bytes))
@@ -83,8 +91,7 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
.execute(conn)
.await?;
let _ = client_id;
Ok(Some(current_nonce))
Ok(Some((client_id, current_nonce)))
})
})
.await
@@ -213,23 +220,25 @@ where
pub async fn authenticate<T>(
props: &mut ClientConnection,
transport: &mut T,
) -> Result<VerifyingKey, Error>
) -> Result<AuthenticatedClient, Error>
where
T: Bi<Inbound, Result<Outbound, Error>> + 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 })
}

View File

@@ -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<auth::Inbound, Result<auth::Outbound, auth::Error>> + 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");
}

View File

@@ -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<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 {
@@ -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,
}

View File

@@ -36,7 +36,10 @@ impl Error {
pub struct UserAgentSession {
props: UserAgentConnection,
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>>,
}
@@ -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<dyn Sender<OutOfBand>>) -> Self {

View File

@@ -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<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
#[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 {

View File

@@ -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 {

View File

@@ -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<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(
mut bi: GrpcBi<ClientRequest, ClientResponse>,
actor: ActorRef<ClientSession>,
@@ -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

View File

@@ -151,7 +151,9 @@ impl Receiver<auth::Inbound> 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<i32>,
) -> 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(())
}

View File

@@ -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<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>);
#[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