Compare commits
1 Commits
6987e5f70f
...
terrors-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efb11d2271 |
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
6
server/Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
127
server/crates/arbiter-client/src/errors.rs
Normal file
127
server/crates/arbiter-client/src/errors.rs
Normal 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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,17 +21,20 @@ 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()
|
|
||||||
.map_err(|v: Vec<u8>| StorageError::InvalidKeyLength {
|
|
||||||
expected: 32,
|
expected: 32,
|
||||||
actual: v.len(),
|
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");
|
||||||
|
|||||||
@@ -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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}"
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,8 +14,7 @@ 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::{
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user