1 Commits

Author SHA1 Message Date
CleverWild
efb11d2271 refactor(arbiter-client): rewrite errors to terrros 2026-03-24 17:25:45 +01:00
22 changed files with 275 additions and 726 deletions

View File

@@ -42,7 +42,6 @@ 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,11 +137,6 @@ 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 {
@@ -160,7 +155,6 @@ 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 {
@@ -181,6 +175,5 @@ 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;
} }
} }

6
server/Cargo.lock generated
View File

@@ -686,6 +686,7 @@ dependencies = [
"http", "http",
"rand 0.10.0", "rand 0.10.0",
"rustls-webpki", "rustls-webpki",
"terrors",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
@@ -4908,6 +4909,11 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "terrors"
version = "0.5.1"
source = "git+https://github.com/CleverWild/terrors#a0867fd9ca3fbb44c32e92113a917f1577b5716a"
[[package]] [[package]]
name = "test-log" name = "test-log"
version = "0.2.19" version = "0.2.19"

View File

@@ -1,7 +1,5 @@
[workspace] [workspace]
members = [ members = ["crates/*"]
"crates/*",
]
resolver = "3" resolver = "3"
[workspace.lints.clippy] [workspace.lints.clippy]
@@ -43,3 +41,4 @@ k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
rsa = { version = "0.9", features = ["sha2"] } rsa = { version = "0.9", features = ["sha2"] }
sha2 = "0.10" sha2 = "0.10"
spki = "0.7" spki = "0.7"
terrors = { version = "0.5", git = "https://github.com/CleverWild/terrors" }

View File

@@ -24,3 +24,4 @@ http = "1.4.0"
rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] } rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] }
async-trait.workspace = true async-trait.workspace = true
rand.workspace = true rand.workspace = true
terrors.workspace = true

View File

@@ -7,54 +7,15 @@ use arbiter_proto::{
}, },
}; };
use ed25519_dalek::Signer as _; use ed25519_dalek::Signer as _;
use terrors::OneOf;
use crate::{ use crate::{
storage::StorageError, errors::{
ConnectError, MissingAuthChallengeError, UnexpectedAuthResponseError, map_auth_code_error,
},
transport::{ClientTransport, next_request_id}, transport::{ClientTransport, next_request_id},
}; };
#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
#[error("Could not establish connection")]
Connection(#[from] tonic::transport::Error),
#[error("Invalid server URI")]
InvalidUri(#[from] http::uri::InvalidUri),
#[error("Invalid CA certificate")]
InvalidCaCert(#[from] webpki::Error),
#[error("gRPC error")]
Grpc(#[from] tonic::Status),
#[error("Auth challenge was not returned by server")]
MissingAuthChallenge,
#[error("Client approval denied by User Agent")]
ApprovalDenied,
#[error("No User Agents online to approve client")]
NoUserAgentsOnline,
#[error("Unexpected auth response payload")]
UnexpectedAuthResponse,
#[error("Signing key storage error")]
Storage(#[from] StorageError),
}
fn map_auth_result(code: i32) -> ConnectError {
match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) {
AuthResult::ApprovalDenied => ConnectError::ApprovalDenied,
AuthResult::NoUserAgentsOnline => ConnectError::NoUserAgentsOnline,
AuthResult::Unspecified
| AuthResult::Success
| AuthResult::InvalidKey
| AuthResult::InvalidSignature
| AuthResult::Internal => ConnectError::UnexpectedAuthResponse,
}
}
async fn send_auth_challenge_request( async fn send_auth_challenge_request(
transport: &mut ClientTransport, transport: &mut ClientTransport,
key: &ed25519_dalek::SigningKey, key: &ed25519_dalek::SigningKey,
@@ -69,7 +30,7 @@ async fn send_auth_challenge_request(
)), )),
}) })
.await .await
.map_err(|_| ConnectError::UnexpectedAuthResponse) .map_err(|_| OneOf::new(UnexpectedAuthResponseError))
} }
async fn receive_auth_challenge( async fn receive_auth_challenge(
@@ -78,13 +39,15 @@ async fn receive_auth_challenge(
let response = transport let response = transport
.recv() .recv()
.await .await
.map_err(|_| ConnectError::MissingAuthChallenge)?; .map_err(|_| OneOf::new(MissingAuthChallengeError))?;
let payload = response.payload.ok_or(ConnectError::MissingAuthChallenge)?; let payload = response
.payload
.ok_or_else(|| OneOf::new(MissingAuthChallengeError))?;
match payload { match payload {
ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge), ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge),
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)), ClientResponsePayload::AuthResult(result) => Err(map_auth_code_error(result)),
_ => Err(ConnectError::UnexpectedAuthResponse), _ => Err(OneOf::new(UnexpectedAuthResponseError)),
} }
} }
@@ -104,7 +67,7 @@ async fn send_auth_challenge_solution(
)), )),
}) })
.await .await
.map_err(|_| ConnectError::UnexpectedAuthResponse) .map_err(|_| OneOf::new(UnexpectedAuthResponseError))
} }
async fn receive_auth_confirmation( async fn receive_auth_confirmation(
@@ -113,19 +76,19 @@ async fn receive_auth_confirmation(
let response = transport let response = transport
.recv() .recv()
.await .await
.map_err(|_| ConnectError::UnexpectedAuthResponse)?; .map_err(|_| OneOf::new(UnexpectedAuthResponseError))?;
let payload = response let payload = response
.payload .payload
.ok_or(ConnectError::UnexpectedAuthResponse)?; .ok_or_else(|| OneOf::new(UnexpectedAuthResponseError))?;
match payload { match payload {
ClientResponsePayload::AuthResult(result) ClientResponsePayload::AuthResult(result)
if AuthResult::try_from(result).ok() == Some(AuthResult::Success) => if AuthResult::try_from(result).ok() == Some(AuthResult::Success) =>
{ {
Ok(()) Ok(())
} }
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)), ClientResponsePayload::AuthResult(result) => Err(map_auth_code_error(result)),
_ => Err(ConnectError::UnexpectedAuthResponse), _ => Err(OneOf::new(UnexpectedAuthResponseError)),
} }
} }

View File

@@ -1,27 +1,23 @@
use arbiter_proto::{proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl}; use arbiter_proto::{proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl};
use std::sync::Arc; use std::sync::Arc;
use terrors::{Broaden as _, OneOf};
use tokio::sync::{Mutex, mpsc}; use tokio::sync::{Mutex, mpsc};
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::ClientTlsConfig; use tonic::transport::ClientTlsConfig;
use crate::{ use crate::{
auth::{ConnectError, authenticate}, auth::authenticate,
errors::ConnectError,
storage::{FileSigningKeyStorage, SigningKeyStorage}, storage::{FileSigningKeyStorage, SigningKeyStorage},
transport::{BUFFER_LENGTH, ClientTransport}, transport::{BUFFER_LENGTH, ClientTransport},
}; };
#[cfg(feature = "evm")]
use crate::errors::{ClientConnectionClosedError, ClientError};
#[cfg(feature = "evm")] #[cfg(feature = "evm")]
use crate::wallets::evm::ArbiterEvmWallet; use crate::wallets::evm::ArbiterEvmWallet;
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("gRPC error")]
Grpc(#[from] tonic::Status),
#[error("Connection closed by server")]
ConnectionClosed,
}
pub struct ArbiterClient { pub struct ArbiterClient {
#[allow(dead_code)] #[allow(dead_code)]
transport: Arc<Mutex<ClientTransport>>, transport: Arc<Mutex<ClientTransport>>,
@@ -29,7 +25,7 @@ pub struct ArbiterClient {
impl ArbiterClient { impl ArbiterClient {
pub async fn connect(url: ArbiterUrl) -> Result<Self, ConnectError> { pub async fn connect(url: ArbiterUrl) -> Result<Self, ConnectError> {
let storage = FileSigningKeyStorage::from_default_location()?; let storage = FileSigningKeyStorage::from_default_location().broaden()?;
Self::connect_with_storage(url, &storage).await Self::connect_with_storage(url, &storage).await
} }
@@ -37,7 +33,7 @@ impl ArbiterClient {
url: ArbiterUrl, url: ArbiterUrl,
storage: &S, storage: &S,
) -> Result<Self, ConnectError> { ) -> Result<Self, ConnectError> {
let key = storage.load_or_create()?; let key = storage.load_or_create().broaden()?;
Self::connect_with_key(url, key).await Self::connect_with_key(url, key).await
} }
@@ -45,17 +41,26 @@ impl ArbiterClient {
url: ArbiterUrl, url: ArbiterUrl,
key: ed25519_dalek::SigningKey, key: ed25519_dalek::SigningKey,
) -> Result<Self, ConnectError> { ) -> Result<Self, ConnectError> {
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned(); let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)
.map_err(OneOf::new)?
.to_owned();
let tls = ClientTlsConfig::new().trust_anchor(anchor); let tls = ClientTlsConfig::new().trust_anchor(anchor);
let channel = tonic::transport::Channel::from_shared(format!("{}:{}", url.host, url.port))? let channel = tonic::transport::Channel::from_shared(format!("{}:{}", url.host, url.port))
.tls_config(tls)? .map_err(OneOf::new)?
.tls_config(tls)
.map_err(OneOf::new)?
.connect() .connect()
.await?; .await
.map_err(OneOf::new)?;
let mut client = ArbiterServiceClient::new(channel); let mut client = ArbiterServiceClient::new(channel);
let (tx, rx) = mpsc::channel(BUFFER_LENGTH); let (tx, rx) = mpsc::channel(BUFFER_LENGTH);
let response_stream = client.client(ReceiverStream::new(rx)).await?.into_inner(); let response_stream = client
.client(ReceiverStream::new(rx))
.await
.map_err(OneOf::new)?
.into_inner();
let mut transport = ClientTransport { let mut transport = ClientTransport {
sender: tx, sender: tx,
@@ -71,6 +76,7 @@ impl ArbiterClient {
#[cfg(feature = "evm")] #[cfg(feature = "evm")]
pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, ClientError> { pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, ClientError> {
todo!("fetch EVM wallet list from server") let _ = &self.transport;
Err(OneOf::new(ClientConnectionClosedError))
} }
} }

View File

@@ -0,0 +1,127 @@
use terrors::OneOf;
use thiserror::Error;
#[cfg(feature = "evm")]
use alloy::{primitives::ChainId, signers::Error as AlloySignerError};
pub type StorageError = OneOf<(std::io::Error, InvalidKeyLengthError)>;
pub type ConnectError = OneOf<(
tonic::transport::Error,
http::uri::InvalidUri,
webpki::Error,
tonic::Status,
MissingAuthChallengeError,
ApprovalDeniedError,
NoUserAgentsOnlineError,
UnexpectedAuthResponseError,
std::io::Error,
InvalidKeyLengthError,
)>;
pub type ClientError = OneOf<(tonic::Status, ClientConnectionClosedError)>;
pub(crate) type ClientTransportError =
OneOf<(TransportChannelClosedError, TransportConnectionClosedError)>;
#[cfg(feature = "evm")]
pub(crate) type EvmWalletError = OneOf<(
EvmChainIdMismatchError,
EvmHashSigningUnsupportedError,
EvmTransactionSigningUnsupportedError,
)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Invalid signing key length in storage: expected {expected} bytes, got {actual} bytes")]
pub struct InvalidKeyLengthError {
pub expected: usize,
pub actual: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Auth challenge was not returned by server")]
pub struct MissingAuthChallengeError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Client approval denied by User Agent")]
pub struct ApprovalDeniedError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("No User Agents online to approve client")]
pub struct NoUserAgentsOnlineError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Unexpected auth response payload")]
pub struct UnexpectedAuthResponseError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Connection closed by server")]
pub struct ClientConnectionClosedError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Transport channel closed")]
pub struct TransportChannelClosedError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Connection closed by server")]
pub struct TransportConnectionClosedError;
#[cfg(feature = "evm")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Transaction chain id mismatch: signer {signer}, tx {tx}")]
pub struct EvmChainIdMismatchError {
pub signer: ChainId,
pub tx: ChainId,
}
#[cfg(feature = "evm")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("hash-only signing is not supported for ArbiterEvmWallet; use transaction signing")]
pub struct EvmHashSigningUnsupportedError;
#[cfg(feature = "evm")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("transaction signing is not supported by current arbiter.client protocol")]
pub struct EvmTransactionSigningUnsupportedError;
pub(crate) fn map_auth_code_error(code: i32) -> ConnectError {
use arbiter_proto::proto::client::AuthResult;
match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) {
AuthResult::ApprovalDenied => OneOf::new(ApprovalDeniedError),
AuthResult::NoUserAgentsOnline => OneOf::new(NoUserAgentsOnlineError),
AuthResult::Unspecified
| AuthResult::Success
| AuthResult::InvalidKey
| AuthResult::InvalidSignature
| AuthResult::Internal => OneOf::new(UnexpectedAuthResponseError),
}
}
#[cfg(feature = "evm")]
impl From<EvmChainIdMismatchError> for AlloySignerError {
fn from(value: EvmChainIdMismatchError) -> Self {
AlloySignerError::TransactionChainIdMismatch {
signer: value.signer,
tx: value.tx,
}
}
}
#[cfg(feature = "evm")]
impl From<EvmHashSigningUnsupportedError> for AlloySignerError {
fn from(_value: EvmHashSigningUnsupportedError) -> Self {
AlloySignerError::other(
"hash-only signing is not supported for ArbiterEvmWallet; use transaction signing",
)
}
}
#[cfg(feature = "evm")]
impl From<EvmTransactionSigningUnsupportedError> for AlloySignerError {
fn from(_value: EvmTransactionSigningUnsupportedError) -> Self {
AlloySignerError::other(
"transaction signing is not supported by current arbiter.client protocol",
)
}
}

View File

@@ -1,12 +1,13 @@
mod auth; mod auth;
mod client; mod client;
mod errors;
mod storage; mod storage;
mod transport; mod transport;
pub mod wallets; pub mod wallets;
pub use auth::ConnectError; pub use client::ArbiterClient;
pub use client::{ArbiterClient, ClientError}; pub use errors::{ClientError, ConnectError, StorageError};
pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError}; pub use storage::{FileSigningKeyStorage, SigningKeyStorage};
#[cfg(feature = "evm")] #[cfg(feature = "evm")]
pub use wallets::evm::ArbiterEvmWallet; pub use wallets::evm::ArbiterEvmWallet;

View File

@@ -1,14 +1,8 @@
use arbiter_proto::home_path; use arbiter_proto::home_path;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use terrors::OneOf;
#[derive(Debug, thiserror::Error)] use crate::errors::{InvalidKeyLengthError, StorageError};
pub enum StorageError {
#[error("I/O error")]
Io(#[from] std::io::Error),
#[error("Invalid signing key length in storage: expected {expected} bytes, got {actual} bytes")]
InvalidKeyLength { expected: usize, actual: usize },
}
pub trait SigningKeyStorage { pub trait SigningKeyStorage {
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError>; fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError>;
@@ -27,18 +21,21 @@ impl FileSigningKeyStorage {
} }
pub fn from_default_location() -> std::result::Result<Self, StorageError> { pub fn from_default_location() -> std::result::Result<Self, StorageError> {
Ok(Self::new(home_path()?.join(Self::DEFAULT_FILE_NAME))) Ok(Self::new(
home_path()
.map_err(OneOf::new)?
.join(Self::DEFAULT_FILE_NAME),
))
} }
fn read_key(path: &Path) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> { fn read_key(path: &Path) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
let bytes = std::fs::read(path)?; let bytes = std::fs::read(path).map_err(OneOf::new)?;
let raw: [u8; 32] = let raw: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
bytes OneOf::new(InvalidKeyLengthError {
.try_into() expected: 32,
.map_err(|v: Vec<u8>| StorageError::InvalidKeyLength { actual: v.len(),
expected: 32, })
actual: v.len(), })?;
})?;
Ok(ed25519_dalek::SigningKey::from_bytes(&raw)) Ok(ed25519_dalek::SigningKey::from_bytes(&raw))
} }
} }
@@ -46,7 +43,7 @@ impl FileSigningKeyStorage {
impl SigningKeyStorage for FileSigningKeyStorage { impl SigningKeyStorage for FileSigningKeyStorage {
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> { fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
if let Some(parent) = self.path.parent() { if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent).map_err(OneOf::new)?;
} }
if self.path.exists() { if self.path.exists() {
@@ -64,20 +61,21 @@ impl SigningKeyStorage for FileSigningKeyStorage {
{ {
Ok(mut file) => { Ok(mut file) => {
use std::io::Write as _; use std::io::Write as _;
file.write_all(&raw_key)?; file.write_all(&raw_key).map_err(OneOf::new)?;
Ok(key) Ok(key)
} }
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
Self::read_key(&self.path) Self::read_key(&self.path)
} }
Err(err) => Err(StorageError::Io(err)), Err(err) => Err(OneOf::new(err)),
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{FileSigningKeyStorage, SigningKeyStorage, StorageError}; use super::{FileSigningKeyStorage, SigningKeyStorage};
use crate::errors::InvalidKeyLengthError;
fn unique_temp_key_path() -> std::path::PathBuf { fn unique_temp_key_path() -> std::path::PathBuf {
let nanos = std::time::SystemTime::now() let nanos = std::time::SystemTime::now()
@@ -119,12 +117,12 @@ mod tests {
.load_or_create() .load_or_create()
.expect_err("storage should reject non-32-byte key file"); .expect_err("storage should reject non-32-byte key file");
match err { match err.narrow::<InvalidKeyLengthError, _>() {
StorageError::InvalidKeyLength { expected, actual } => { Ok(invalid_len) => {
assert_eq!(expected, 32); assert_eq!(invalid_len.expected, 32);
assert_eq!(actual, 31); assert_eq!(invalid_len.actual, 31);
} }
other => panic!("unexpected error: {other:?}"), Err(other) => panic!("unexpected io error: {other:?}"),
} }
std::fs::remove_file(path).expect("temp key file should be removable"); std::fs::remove_file(path).expect("temp key file should be removable");

View File

@@ -1,7 +1,12 @@
use arbiter_proto::proto::client::{ClientRequest, ClientResponse}; use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::atomic::{AtomicI32, Ordering};
use terrors::OneOf;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::errors::{
ClientTransportError, TransportChannelClosedError, TransportConnectionClosedError,
};
pub(crate) const BUFFER_LENGTH: usize = 16; pub(crate) const BUFFER_LENGTH: usize = 16;
static NEXT_REQUEST_ID: AtomicI32 = AtomicI32::new(1); static NEXT_REQUEST_ID: AtomicI32 = AtomicI32::new(1);
@@ -9,15 +14,6 @@ pub(crate) fn next_request_id() -> i32 {
NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed) NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed)
} }
#[derive(Debug, thiserror::Error)]
pub(crate) enum ClientSignError {
#[error("Transport channel closed")]
ChannelClosed,
#[error("Connection closed by server")]
ConnectionClosed,
}
pub(crate) struct ClientTransport { pub(crate) struct ClientTransport {
pub(crate) sender: mpsc::Sender<ClientRequest>, pub(crate) sender: mpsc::Sender<ClientRequest>,
pub(crate) receiver: tonic::Streaming<ClientResponse>, pub(crate) receiver: tonic::Streaming<ClientResponse>,
@@ -27,18 +23,20 @@ impl ClientTransport {
pub(crate) async fn send( pub(crate) async fn send(
&mut self, &mut self,
request: ClientRequest, request: ClientRequest,
) -> std::result::Result<(), ClientSignError> { ) -> std::result::Result<(), ClientTransportError> {
self.sender self.sender
.send(request) .send(request)
.await .await
.map_err(|_| ClientSignError::ChannelClosed) .map_err(|_| OneOf::new(TransportChannelClosedError))
} }
pub(crate) async fn recv(&mut self) -> std::result::Result<ClientResponse, ClientSignError> { pub(crate) async fn recv(
&mut self,
) -> std::result::Result<ClientResponse, ClientTransportError> {
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(OneOf::new(TransportConnectionClosedError)),
Err(_) => Err(ClientSignError::ConnectionClosed), Err(_) => Err(OneOf::new(TransportConnectionClosedError)),
} }
} }
} }

View File

@@ -2,22 +2,21 @@ use alloy::{
consensus::SignableTransaction, consensus::SignableTransaction,
network::TxSigner, network::TxSigner,
primitives::{Address, B256, ChainId, Signature}, primitives::{Address, B256, ChainId, Signature},
signers::{Error, Result, Signer}, signers::{Result, Signer},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use std::sync::Arc; use std::sync::Arc;
use terrors::OneOf;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use arbiter_proto::proto::{ use crate::{
client::{ errors::{
ClientRequest, client_request::Payload as ClientRequestPayload, EvmChainIdMismatchError, EvmHashSigningUnsupportedError,
client_response::Payload as ClientResponsePayload, EvmTransactionSigningUnsupportedError, EvmWalletError,
}, },
evm::evm_sign_transaction_response::Result as EvmSignTransactionResult, transport::ClientTransport,
}; };
use crate::transport::{ClientTransport, next_request_id};
pub struct ArbiterEvmWallet { pub struct ArbiterEvmWallet {
transport: Arc<Mutex<ClientTransport>>, transport: Arc<Mutex<ClientTransport>>,
address: Address, address: Address,
@@ -25,6 +24,7 @@ pub struct ArbiterEvmWallet {
} }
impl ArbiterEvmWallet { impl ArbiterEvmWallet {
#[allow(dead_code)]
pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self { pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
Self { Self {
transport, transport,
@@ -42,14 +42,17 @@ impl ArbiterEvmWallet {
self self
} }
fn validate_chain_id(&self, tx: &mut dyn SignableTransaction<Signature>) -> Result<()> { fn validate_chain_id(
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> std::result::Result<(), EvmWalletError> {
if let Some(chain_id) = self.chain_id if let Some(chain_id) = self.chain_id
&& !tx.set_chain_id_checked(chain_id) && !tx.set_chain_id_checked(chain_id)
{ {
return Err(Error::TransactionChainIdMismatch { return Err(OneOf::new(EvmChainIdMismatchError {
signer: chain_id, signer: chain_id,
tx: tx.chain_id().unwrap(), tx: tx.chain_id().unwrap(),
}); }));
} }
Ok(()) Ok(())
@@ -59,9 +62,7 @@ impl ArbiterEvmWallet {
#[async_trait] #[async_trait]
impl Signer for ArbiterEvmWallet { impl Signer for ArbiterEvmWallet {
async fn sign_hash(&self, _hash: &B256) -> Result<Signature> { async fn sign_hash(&self, _hash: &B256) -> Result<Signature> {
Err(Error::other( Err(EvmWalletError::new(EvmHashSigningUnsupportedError).into())
"hash-only signing is not supported for ArbiterEvmWallet; use transaction signing",
))
} }
fn address(&self) -> Address { fn address(&self) -> Address {
@@ -87,61 +88,10 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
&self, &self,
tx: &mut dyn SignableTransaction<Signature>, tx: &mut dyn SignableTransaction<Signature>,
) -> Result<Signature> { ) -> Result<Signature> {
self.validate_chain_id(tx)?; let _transport = self.transport.lock().await;
self.validate_chain_id(tx)
.map_err(OneOf::into::<alloy::signers::Error>)?;
let mut transport = self.transport.lock().await; Err(EvmWalletError::new(EvmTransactionSigningUnsupportedError).into())
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,19 +54,10 @@ 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( async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Option<i32>, Error> {
db: &db::DatabasePool, let pubkey_bytes = pubkey.as_bytes().to_vec();
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");
@@ -74,6 +65,7 @@ async fn get_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))
@@ -91,7 +83,8 @@ async fn get_nonce(
.execute(conn) .execute(conn)
.await?; .await?;
Ok(Some((client_id, current_nonce))) let _ = client_id;
Ok(Some(current_nonce))
}) })
}) })
.await .await
@@ -220,25 +213,23 @@ where
pub async fn authenticate<T>( pub async fn authenticate<T>(
props: &mut ClientConnection, props: &mut ClientConnection,
transport: &mut T, transport: &mut T,
) -> Result<AuthenticatedClient, Error> ) -> Result<VerifyingKey, 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 else { let Some(Inbound::AuthChallengeRequest { pubkey }) = transport.recv().await
else {
return Err(Error::Transport); return Err(Error::Transport);
}; };
let (client_id, nonce) = match get_nonce(&props.db, &pubkey).await? { let nonce = match get_nonce(&props.db, &pubkey).await? {
Some(client_nonce) => client_nonce, Some(nonce) => 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 => match get_nonce(&props.db, &pubkey).await? { InsertClientResult::Inserted => 0,
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((client_id, nonce)) => (client_id, nonce), Some(nonce) => nonce,
None => return Err(Error::DatabaseOperationFailed), None => return Err(Error::DatabaseOperationFailed),
}, },
} }
@@ -254,5 +245,5 @@ where
Error::Transport Error::Transport
})?; })?;
Ok(AuthenticatedClient { pubkey, client_id }) Ok(pubkey)
} }

View File

@@ -10,16 +10,11 @@ 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 { Self { db, actors }
db,
actors,
client_id: 0,
}
} }
} }
@@ -31,8 +26,7 @@ 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(authenticated) => { Ok(_pubkey) => {
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,18 +1,11 @@
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, GlobalActors, client::ClientConnection, keyholder::KeyHolderState, router::RegisterClient,
client::ClientConnection,
evm::{ClientSignTransaction, SignTransactionError},
keyholder::KeyHolderState,
router::RegisterClient,
}, },
db, db,
evm::VetError,
}; };
pub struct ClientSession { pub struct ClientSession {
@@ -41,34 +34,6 @@ 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 {
@@ -104,12 +69,3 @@ 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,10 +36,7 @@ impl Error {
pub struct UserAgentSession { pub struct UserAgentSession {
props: UserAgentConnection, props: UserAgentConnection,
state: UserAgentStateMachine<DummyContext>, state: UserAgentStateMachine<DummyContext>,
#[allow( #[allow(dead_code, reason = "The session keeps ownership of the outbound transport even before the state-machine flow starts using it directly")]
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>>,
} }
@@ -47,11 +44,8 @@ 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::{consensus::TxEip1559, primitives::Address, signers::Signature}; use alloy::primitives::Address;
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,14 +14,13 @@ use crate::safe_cell::SafeCell;
use crate::{ use crate::{
actors::{ actors::{
evm::{ evm::{
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError, Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
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 _,
}; };
@@ -104,15 +103,6 @@ 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]
@@ -361,33 +351,4 @@ 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 {
pub(crate) to: Address, to: Address,
pub(crate) value: U256, 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 {
pub(crate) token: &'static TokenInfo, token: &'static TokenInfo,
pub(crate) to: Address, to: Address,
pub(crate) value: U256, 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,5 +1,4 @@
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,
@@ -18,135 +17,16 @@ use crate::{
actors::{ actors::{
client::{ client::{
self, ClientConnection, self, ClientConnection,
session::{ session::{ClientSession, Error, HandleQueryVaultState},
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>,
@@ -210,64 +90,6 @@ 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,9 +151,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
_ => { _ => {
let _ = self let _ = self
.bi .bi
.send(Err(Status::invalid_argument( .send(Err(Status::invalid_argument("Unsupported client auth request")))
"Unsupported client auth request",
)))
.await; .await;
None None
} }
@@ -170,7 +168,6 @@ 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);
let authenticated = client::auth::authenticate(conn, &mut transport).await?; client::auth::authenticate(conn, &mut transport).await?;
conn.client_id = authenticated.client_id;
Ok(()) Ok(())
} }

View File

@@ -4,24 +4,17 @@ 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, EtherTransferSettings as ProtoEtherTransferSettings, EvmError as ProtoEvmError,
EvalViolation as ProtoEvalViolation, EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateRequest, EvmGrantCreateResponse, EvmGrantDeleteRequest,
EvmGrantCreateResponse, EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, GrantEntry,
EvmGrantListResponse, EvmSignTransactionResponse, GasLimitExceededViolation,
GrantEntry, NoMatchingGrantError, PolicyViolationsError,
SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant, SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant,
SpecificMeaning as ProtoSpecificMeaning, TokenInfo as ProtoTokenInfo, TokenTransferSettings as ProtoTokenTransferSettings,
TokenTransferSettings as ProtoTokenTransferSettings, TransactionEvalError,
TransactionRateLimit as ProtoTransactionRateLimit, TransactionRateLimit as ProtoTransactionRateLimit,
VolumeRateLimit as ProtoVolumeRateLimit, WalletCreateResponse, WalletEntry, WalletList, VolumeRateLimit as ProtoVolumeRateLimit, WalletCreateResponse, WalletEntry, WalletList,
WalletListResponse, eval_violation::Kind as ProtoEvalViolationKind, WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult,
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,
}, },
@@ -30,8 +23,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, UserAgentEvmSignTransactionRequest, UserAgentRequest, UserAgentResponse, UnsealStart, UserAgentRequest, UserAgentResponse, VaultState as ProtoVaultState,
VaultState as ProtoVaultState, user_agent_request::Payload as UserAgentRequestPayload, user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload, user_agent_response::Payload as UserAgentResponsePayload,
}, },
}, },
@@ -54,9 +47,7 @@ use crate::{
session::{ session::{
BootstrapError, Error, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, BootstrapError, Error, HandleBootstrapEncryptedKey, HandleEvmWalletCreate,
HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantList, HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantList,
HandleQueryVaultState, HandleSignTransaction, HandleUnsealEncryptedKey, HandleQueryVaultState, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
HandleUnsealRequest, SignTransactionError as SessionSignTransactionError,
UnsealError,
}, },
}, },
}, },
@@ -64,124 +55,12 @@ 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::{ use alloy::primitives::{Address, U256};
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]
@@ -392,92 +271,6 @@ 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