feat(evm): implement EVM sign transaction handling in client and user agent
This commit is contained in:
@@ -42,6 +42,7 @@ message ClientRequest {
|
||||
AuthChallengeRequest auth_challenge_request = 1;
|
||||
AuthChallengeSolution auth_challenge_solution = 2;
|
||||
google.protobuf.Empty query_vault_state = 3;
|
||||
arbiter.evm.EvmSignTransactionRequest evm_sign_transaction = 5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,11 @@ message SdkClientConnectionResponse {
|
||||
|
||||
message SdkClientConnectionCancel {}
|
||||
|
||||
message UserAgentEvmSignTransactionRequest {
|
||||
int32 client_id = 1;
|
||||
arbiter.evm.EvmSignTransactionRequest request = 2;
|
||||
}
|
||||
|
||||
message UserAgentRequest {
|
||||
int32 id = 16;
|
||||
oneof payload {
|
||||
@@ -155,6 +160,7 @@ message UserAgentRequest {
|
||||
SdkClientRevokeRequest sdk_client_revoke = 13;
|
||||
google.protobuf.Empty sdk_client_list = 14;
|
||||
BootstrapEncryptedKey bootstrap_encrypted_key = 15;
|
||||
UserAgentEvmSignTransactionRequest evm_sign_transaction = 17;
|
||||
}
|
||||
}
|
||||
message UserAgentResponse {
|
||||
@@ -175,5 +181,6 @@ message UserAgentResponse {
|
||||
SdkClientRevokeResponse sdk_client_revoke_response = 13;
|
||||
SdkClientListResponse sdk_client_list_response = 14;
|
||||
BootstrapResult bootstrap_result = 15;
|
||||
arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 17;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use arbiter_proto::proto::{
|
||||
client::{ClientRequest, ClientResponse},
|
||||
};
|
||||
use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
@@ -36,9 +34,7 @@ impl ClientTransport {
|
||||
.map_err(|_| ClientSignError::ChannelClosed)
|
||||
}
|
||||
|
||||
pub(crate) async fn recv(
|
||||
&mut self,
|
||||
) -> std::result::Result<ClientResponse, ClientSignError> {
|
||||
pub(crate) async fn recv(&mut self) -> std::result::Result<ClientResponse, ClientSignError> {
|
||||
match self.receiver.message().await {
|
||||
Ok(Some(resp)) => Ok(resp),
|
||||
Ok(None) => Err(ClientSignError::ConnectionClosed),
|
||||
|
||||
@@ -8,7 +8,15 @@ use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::transport::ClientTransport;
|
||||
use arbiter_proto::proto::{
|
||||
client::{
|
||||
ClientRequest, client_request::Payload as ClientRequestPayload,
|
||||
client_response::Payload as ClientResponsePayload,
|
||||
},
|
||||
evm::evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||
};
|
||||
|
||||
use crate::transport::{ClientTransport, next_request_id};
|
||||
|
||||
pub struct ArbiterEvmWallet {
|
||||
transport: Arc<Mutex<ClientTransport>>,
|
||||
@@ -79,11 +87,61 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
|
||||
&self,
|
||||
tx: &mut dyn SignableTransaction<Signature>,
|
||||
) -> Result<Signature> {
|
||||
let _transport = self.transport.lock().await;
|
||||
self.validate_chain_id(tx)?;
|
||||
|
||||
Err(Error::other(
|
||||
"transaction signing is not supported by current arbiter.client protocol",
|
||||
))
|
||||
let mut transport = self.transport.lock().await;
|
||||
let request_id = next_request_id();
|
||||
let rlp_transaction = tx.encoded_for_signing();
|
||||
|
||||
transport
|
||||
.send(ClientRequest {
|
||||
request_id,
|
||||
payload: Some(ClientRequestPayload::EvmSignTransaction(
|
||||
arbiter_proto::proto::evm::EvmSignTransactionRequest {
|
||||
wallet_address: self.address.to_vec(),
|
||||
rlp_transaction,
|
||||
},
|
||||
)),
|
||||
})
|
||||
.await
|
||||
.map_err(|_| Error::other("failed to send evm sign transaction request"))?;
|
||||
|
||||
let response = transport
|
||||
.recv()
|
||||
.await
|
||||
.map_err(|_| Error::other("failed to receive evm sign transaction response"))?;
|
||||
|
||||
if response.request_id != Some(request_id) {
|
||||
return Err(Error::other(
|
||||
"received mismatched response id for evm sign transaction",
|
||||
));
|
||||
}
|
||||
|
||||
let payload = response
|
||||
.payload
|
||||
.ok_or_else(|| Error::other("missing evm sign transaction response payload"))?;
|
||||
|
||||
let ClientResponsePayload::EvmSignTransaction(response) = payload else {
|
||||
return Err(Error::other(
|
||||
"unexpected response payload for evm sign transaction request",
|
||||
));
|
||||
};
|
||||
|
||||
let result = response
|
||||
.result
|
||||
.ok_or_else(|| Error::other("missing evm sign transaction result"))?;
|
||||
|
||||
match result {
|
||||
EvmSignTransactionResult::Signature(signature) => {
|
||||
Signature::try_from(signature.as_slice())
|
||||
.map_err(|_| Error::other("invalid signature returned by server"))
|
||||
}
|
||||
EvmSignTransactionResult::EvalError(eval_error) => Err(Error::other(format!(
|
||||
"transaction rejected by policy: {eval_error:?}"
|
||||
))),
|
||||
EvmSignTransactionResult::Error(code) => Err(Error::other(format!(
|
||||
"server failed to sign transaction with error code {code}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +54,19 @@ pub enum Outbound {
|
||||
AuthSuccess,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthenticatedClient {
|
||||
pub pubkey: VerifyingKey,
|
||||
pub client_id: i32,
|
||||
}
|
||||
|
||||
/// Atomically reads and increments the nonce for a known client.
|
||||
/// Returns `None` if the pubkey is not registered.
|
||||
async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Option<i32>, Error> {
|
||||
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
||||
async fn get_nonce(
|
||||
db: &db::DatabasePool,
|
||||
pubkey: &VerifyingKey,
|
||||
) -> Result<Option<(/* client_id */ i32, /* nonce */ i32)>, Error> {
|
||||
let pubkey_bytes = pubkey.as_bytes();
|
||||
|
||||
let mut conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
@@ -65,7 +74,6 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
|
||||
})?;
|
||||
|
||||
conn.exclusive_transaction(|conn| {
|
||||
let pubkey_bytes = pubkey_bytes.clone();
|
||||
Box::pin(async move {
|
||||
let Some((client_id, current_nonce)) = program_client::table
|
||||
.filter(program_client::public_key.eq(&pubkey_bytes))
|
||||
@@ -83,8 +91,7 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
|
||||
.execute(conn)
|
||||
.await?;
|
||||
|
||||
let _ = client_id;
|
||||
Ok(Some(current_nonce))
|
||||
Ok(Some((client_id, current_nonce)))
|
||||
})
|
||||
})
|
||||
.await
|
||||
@@ -213,23 +220,25 @@ where
|
||||
pub async fn authenticate<T>(
|
||||
props: &mut ClientConnection,
|
||||
transport: &mut T,
|
||||
) -> Result<VerifyingKey, Error>
|
||||
) -> Result<AuthenticatedClient, Error>
|
||||
where
|
||||
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
|
||||
{
|
||||
let Some(Inbound::AuthChallengeRequest { pubkey }) = transport.recv().await
|
||||
else {
|
||||
let Some(Inbound::AuthChallengeRequest { pubkey }) = transport.recv().await else {
|
||||
return Err(Error::Transport);
|
||||
};
|
||||
|
||||
let nonce = match get_nonce(&props.db, &pubkey).await? {
|
||||
Some(nonce) => nonce,
|
||||
let (client_id, nonce) = match get_nonce(&props.db, &pubkey).await? {
|
||||
Some(client_nonce) => client_nonce,
|
||||
None => {
|
||||
approve_new_client(&props.actors, pubkey).await?;
|
||||
match insert_client(&props.db, &pubkey).await? {
|
||||
InsertClientResult::Inserted => 0,
|
||||
InsertClientResult::Inserted => match get_nonce(&props.db, &pubkey).await? {
|
||||
Some((client_id, _)) => (client_id, 0),
|
||||
None => return Err(Error::DatabaseOperationFailed),
|
||||
},
|
||||
InsertClientResult::AlreadyExists => match get_nonce(&props.db, &pubkey).await? {
|
||||
Some(nonce) => nonce,
|
||||
Some((client_id, nonce)) => (client_id, nonce),
|
||||
None => return Err(Error::DatabaseOperationFailed),
|
||||
},
|
||||
}
|
||||
@@ -245,5 +254,5 @@ where
|
||||
Error::Transport
|
||||
})?;
|
||||
|
||||
Ok(pubkey)
|
||||
Ok(AuthenticatedClient { pubkey, client_id })
|
||||
}
|
||||
|
||||
@@ -10,11 +10,16 @@ use crate::{
|
||||
pub struct ClientConnection {
|
||||
pub(crate) db: db::DatabasePool,
|
||||
pub(crate) actors: GlobalActors,
|
||||
pub(crate) client_id: i32,
|
||||
}
|
||||
|
||||
impl ClientConnection {
|
||||
pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
||||
Self { db, actors }
|
||||
Self {
|
||||
db,
|
||||
actors,
|
||||
client_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +31,8 @@ where
|
||||
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send + ?Sized,
|
||||
{
|
||||
match auth::authenticate(&mut props, transport).await {
|
||||
Ok(_pubkey) => {
|
||||
Ok(authenticated) => {
|
||||
props.client_id = authenticated.client_id;
|
||||
ClientSession::spawn(ClientSession::new(props));
|
||||
info!("Client authenticated, session started");
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
use kameo::{Actor, messages};
|
||||
use tracing::error;
|
||||
|
||||
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
|
||||
|
||||
use crate::{
|
||||
actors::{
|
||||
GlobalActors, client::ClientConnection, keyholder::KeyHolderState, router::RegisterClient,
|
||||
GlobalActors,
|
||||
client::ClientConnection,
|
||||
evm::{ClientSignTransaction, SignTransactionError},
|
||||
keyholder::KeyHolderState,
|
||||
router::RegisterClient,
|
||||
},
|
||||
db,
|
||||
evm::VetError,
|
||||
};
|
||||
|
||||
pub struct ClientSession {
|
||||
@@ -34,6 +41,34 @@ impl ClientSession {
|
||||
|
||||
Ok(vault_state)
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub(crate) async fn handle_sign_transaction(
|
||||
&mut self,
|
||||
wallet_address: Address,
|
||||
transaction: TxEip1559,
|
||||
) -> Result<Signature, SignTransactionRpcError> {
|
||||
match self
|
||||
.props
|
||||
.actors
|
||||
.evm
|
||||
.ask(ClientSignTransaction {
|
||||
client_id: self.props.client_id,
|
||||
wallet_address,
|
||||
transaction,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(signature) => Ok(signature),
|
||||
Err(kameo::error::SendError::HandlerError(SignTransactionError::Vet(vet_error))) => {
|
||||
Err(SignTransactionRpcError::Vet(vet_error))
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to sign EVM transaction in client session");
|
||||
Err(SignTransactionRpcError::Internal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for ClientSession {
|
||||
@@ -69,3 +104,12 @@ pub enum Error {
|
||||
#[error("Internal error")]
|
||||
Internal,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SignTransactionRpcError {
|
||||
#[error("Policy evaluation failed")]
|
||||
Vet(#[from] VetError),
|
||||
|
||||
#[error("Internal error")]
|
||||
Internal,
|
||||
}
|
||||
|
||||
@@ -36,7 +36,10 @@ impl Error {
|
||||
pub struct UserAgentSession {
|
||||
props: UserAgentConnection,
|
||||
state: UserAgentStateMachine<DummyContext>,
|
||||
#[allow(dead_code, reason = "The session keeps ownership of the outbound transport even before the state-machine flow starts using it directly")]
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "The session keeps ownership of the outbound transport even before the state-machine flow starts using it directly"
|
||||
)]
|
||||
sender: Box<dyn Sender<OutOfBand>>,
|
||||
}
|
||||
|
||||
@@ -44,8 +47,11 @@ mod connection;
|
||||
pub(crate) use connection::{
|
||||
BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, HandleEvmWalletList,
|
||||
HandleGrantCreate, HandleGrantDelete, HandleGrantList, HandleQueryVaultState,
|
||||
HandleSignTransaction,
|
||||
};
|
||||
pub use connection::{
|
||||
HandleUnsealEncryptedKey, HandleUnsealRequest, SignTransactionError, UnsealError,
|
||||
};
|
||||
pub use connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError};
|
||||
|
||||
impl UserAgentSession {
|
||||
pub(crate) fn new(props: UserAgentConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use alloy::primitives::Address;
|
||||
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
|
||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||
use kameo::error::SendError;
|
||||
use kameo::messages;
|
||||
@@ -14,7 +14,8 @@ use crate::safe_cell::SafeCell;
|
||||
use crate::{
|
||||
actors::{
|
||||
evm::{
|
||||
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
|
||||
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
|
||||
UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
|
||||
},
|
||||
keyholder::{self, Bootstrap, TryUnseal},
|
||||
user_agent::session::{
|
||||
@@ -103,6 +104,15 @@ pub enum BootstrapError {
|
||||
General(#[from] super::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SignTransactionError {
|
||||
#[error("Policy evaluation failed")]
|
||||
Vet(#[from] crate::evm::VetError),
|
||||
|
||||
#[error("Internal signing error")]
|
||||
Internal,
|
||||
}
|
||||
|
||||
#[messages]
|
||||
impl UserAgentSession {
|
||||
#[message]
|
||||
@@ -351,4 +361,33 @@ impl UserAgentSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub(crate) async fn handle_sign_transaction(
|
||||
&mut self,
|
||||
client_id: i32,
|
||||
wallet_address: Address,
|
||||
transaction: TxEip1559,
|
||||
) -> Result<Signature, SignTransactionError> {
|
||||
match self
|
||||
.props
|
||||
.actors
|
||||
.evm
|
||||
.ask(ClientSignTransaction {
|
||||
client_id,
|
||||
wallet_address,
|
||||
transaction,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(signature) => Ok(signature),
|
||||
Err(SendError::HandlerError(EvmSignError::Vet(vet_error))) => {
|
||||
Err(SignTransactionError::Vet(vet_error))
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "EVM sign transaction failed in user-agent session");
|
||||
Err(SignTransactionError::Internal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ use super::{DatabaseID, EvalContext, EvalViolation};
|
||||
// Plain ether transfer
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Meaning {
|
||||
to: Address,
|
||||
value: U256,
|
||||
pub(crate) to: Address,
|
||||
pub(crate) value: U256,
|
||||
}
|
||||
impl Display for Meaning {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
|
||||
@@ -38,9 +38,9 @@ fn grant_join() -> _ {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Meaning {
|
||||
token: &'static TokenInfo,
|
||||
to: Address,
|
||||
value: U256,
|
||||
pub(crate) token: &'static TokenInfo,
|
||||
pub(crate) to: Address,
|
||||
pub(crate) value: U256,
|
||||
}
|
||||
impl std::fmt::Display for Meaning {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use arbiter_proto::{
|
||||
google::protobuf::Empty as ProtoEmpty,
|
||||
proto::client::{
|
||||
ClientRequest, ClientResponse, VaultState as ProtoVaultState,
|
||||
client_request::Payload as ClientRequestPayload,
|
||||
@@ -17,16 +18,135 @@ use crate::{
|
||||
actors::{
|
||||
client::{
|
||||
self, ClientConnection,
|
||||
session::{ClientSession, Error, HandleQueryVaultState},
|
||||
session::{
|
||||
ClientSession, Error, HandleQueryVaultState, HandleSignTransaction,
|
||||
SignTransactionRpcError,
|
||||
},
|
||||
},
|
||||
keyholder::KeyHolderState,
|
||||
},
|
||||
evm::{PolicyError, VetError, policies::EvalViolation},
|
||||
grpc::request_tracker::RequestTracker,
|
||||
utils::defer,
|
||||
};
|
||||
|
||||
use alloy::{
|
||||
consensus::TxEip1559,
|
||||
primitives::{Address, U256},
|
||||
rlp::Decodable,
|
||||
};
|
||||
use arbiter_proto::proto::evm::{
|
||||
EvmError as ProtoEvmError, EvmSignTransactionResponse, EvalViolation as ProtoEvalViolation,
|
||||
GasLimitExceededViolation, NoMatchingGrantError, PolicyViolationsError,
|
||||
SpecificMeaning as ProtoSpecificMeaning, TokenInfo as ProtoTokenInfo,
|
||||
TransactionEvalError,
|
||||
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||
eval_violation::Kind as ProtoEvalViolationKind,
|
||||
specific_meaning::Meaning as ProtoSpecificMeaningKind,
|
||||
transaction_eval_error::Kind as ProtoTransactionEvalErrorKind,
|
||||
};
|
||||
|
||||
mod auth;
|
||||
|
||||
fn u256_to_proto_bytes(value: U256) -> Vec<u8> {
|
||||
value.to_be_bytes::<32>().to_vec()
|
||||
}
|
||||
|
||||
fn meaning_to_proto(meaning: crate::evm::policies::SpecificMeaning) -> ProtoSpecificMeaning {
|
||||
let kind = match meaning {
|
||||
crate::evm::policies::SpecificMeaning::EtherTransfer(meaning) => {
|
||||
ProtoSpecificMeaningKind::EtherTransfer(arbiter_proto::proto::evm::EtherTransferMeaning {
|
||||
to: meaning.to.to_vec(),
|
||||
value: u256_to_proto_bytes(meaning.value),
|
||||
})
|
||||
}
|
||||
crate::evm::policies::SpecificMeaning::TokenTransfer(meaning) => {
|
||||
ProtoSpecificMeaningKind::TokenTransfer(arbiter_proto::proto::evm::TokenTransferMeaning {
|
||||
token: Some(ProtoTokenInfo {
|
||||
symbol: meaning.token.symbol.to_string(),
|
||||
address: meaning.token.contract.to_vec(),
|
||||
chain_id: meaning.token.chain,
|
||||
}),
|
||||
to: meaning.to.to_vec(),
|
||||
value: u256_to_proto_bytes(meaning.value),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
ProtoSpecificMeaning {
|
||||
meaning: Some(kind),
|
||||
}
|
||||
}
|
||||
|
||||
fn violation_to_proto(violation: EvalViolation) -> ProtoEvalViolation {
|
||||
let kind = match violation {
|
||||
EvalViolation::InvalidTarget { target } => ProtoEvalViolationKind::InvalidTarget(target.to_vec()),
|
||||
EvalViolation::GasLimitExceeded {
|
||||
max_gas_fee_per_gas,
|
||||
max_priority_fee_per_gas,
|
||||
} => ProtoEvalViolationKind::GasLimitExceeded(GasLimitExceededViolation {
|
||||
max_gas_fee_per_gas: max_gas_fee_per_gas.map(u256_to_proto_bytes),
|
||||
max_priority_fee_per_gas: max_priority_fee_per_gas.map(u256_to_proto_bytes),
|
||||
}),
|
||||
EvalViolation::RateLimitExceeded => ProtoEvalViolationKind::RateLimitExceeded(ProtoEmpty {}),
|
||||
EvalViolation::VolumetricLimitExceeded => {
|
||||
ProtoEvalViolationKind::VolumetricLimitExceeded(ProtoEmpty {})
|
||||
}
|
||||
EvalViolation::InvalidTime => ProtoEvalViolationKind::InvalidTime(ProtoEmpty {}),
|
||||
EvalViolation::InvalidTransactionType => {
|
||||
ProtoEvalViolationKind::InvalidTransactionType(ProtoEmpty {})
|
||||
}
|
||||
};
|
||||
|
||||
ProtoEvalViolation { kind: Some(kind) }
|
||||
}
|
||||
|
||||
fn eval_error_to_proto(err: VetError) -> Option<TransactionEvalError> {
|
||||
let kind = match err {
|
||||
VetError::ContractCreationNotSupported => {
|
||||
ProtoTransactionEvalErrorKind::ContractCreationNotSupported(ProtoEmpty {})
|
||||
}
|
||||
VetError::UnsupportedTransactionType => {
|
||||
ProtoTransactionEvalErrorKind::UnsupportedTransactionType(ProtoEmpty {})
|
||||
}
|
||||
VetError::Evaluated(meaning, policy_error) => match policy_error {
|
||||
PolicyError::NoMatchingGrant => {
|
||||
ProtoTransactionEvalErrorKind::NoMatchingGrant(NoMatchingGrantError {
|
||||
meaning: Some(meaning_to_proto(meaning)),
|
||||
})
|
||||
}
|
||||
PolicyError::Violations(violations) => {
|
||||
ProtoTransactionEvalErrorKind::PolicyViolations(PolicyViolationsError {
|
||||
meaning: Some(meaning_to_proto(meaning)),
|
||||
violations: violations.into_iter().map(violation_to_proto).collect(),
|
||||
})
|
||||
}
|
||||
PolicyError::Pool(_) | PolicyError::Database(_) => {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Some(TransactionEvalError { kind: Some(kind) })
|
||||
}
|
||||
|
||||
fn decode_eip1559_transaction(payload: &[u8]) -> Result<TxEip1559, ()> {
|
||||
let mut body = payload;
|
||||
if let Some((prefix, rest)) = payload.split_first()
|
||||
&& *prefix == 0x02
|
||||
{
|
||||
body = rest;
|
||||
}
|
||||
|
||||
let mut cursor = body;
|
||||
let transaction = TxEip1559::decode(&mut cursor).map_err(|_| ())?;
|
||||
if !cursor.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
async fn dispatch_loop(
|
||||
mut bi: GrpcBi<ClientRequest, ClientResponse>,
|
||||
actor: ActorRef<ClientSession>,
|
||||
@@ -90,6 +210,64 @@ async fn dispatch_conn_message(
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
ClientRequestPayload::EvmSignTransaction(request) => {
|
||||
let wallet_address = match <[u8; 20]>::try_from(request.wallet_address.as_slice()) {
|
||||
Ok(address) => Address::from(address),
|
||||
Err(_) => {
|
||||
let _ = bi
|
||||
.send(Err(Status::invalid_argument("Invalid EVM wallet address")))
|
||||
.await;
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
|
||||
let transaction = match decode_eip1559_transaction(&request.rlp_transaction) {
|
||||
Ok(transaction) => transaction,
|
||||
Err(()) => {
|
||||
let _ = bi
|
||||
.send(Err(Status::invalid_argument(
|
||||
"Invalid EIP-1559 RLP transaction",
|
||||
)))
|
||||
.await;
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
|
||||
let response = match actor
|
||||
.ask(HandleSignTransaction {
|
||||
wallet_address,
|
||||
transaction,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(signature) => EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::Signature(signature.as_bytes().to_vec())),
|
||||
},
|
||||
Err(kameo::error::SendError::HandlerError(SignTransactionRpcError::Vet(vet_error))) => {
|
||||
match eval_error_to_proto(vet_error) {
|
||||
Some(eval_error) => EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::EvalError(eval_error)),
|
||||
},
|
||||
None => EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::Error(ProtoEvmError::Internal.into())),
|
||||
},
|
||||
}
|
||||
}
|
||||
Err(kameo::error::SendError::HandlerError(SignTransactionRpcError::Internal)) => {
|
||||
EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::Error(ProtoEvmError::Internal.into())),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(error = ?err, "Failed to sign EVM transaction");
|
||||
EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::Error(ProtoEvmError::Internal.into())),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ClientResponsePayload::EvmSignTransaction(response)
|
||||
}
|
||||
payload => {
|
||||
warn!(?payload, "Unsupported post-auth client request");
|
||||
let _ = bi
|
||||
|
||||
@@ -151,7 +151,9 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
||||
_ => {
|
||||
let _ = self
|
||||
.bi
|
||||
.send(Err(Status::invalid_argument("Unsupported client auth request")))
|
||||
.send(Err(Status::invalid_argument(
|
||||
"Unsupported client auth request",
|
||||
)))
|
||||
.await;
|
||||
None
|
||||
}
|
||||
@@ -168,6 +170,7 @@ pub async fn start(
|
||||
response_id: &mut Option<i32>,
|
||||
) -> Result<(), auth::Error> {
|
||||
let mut transport = AuthTransportAdapter::new(bi, request_tracker, response_id);
|
||||
client::auth::authenticate(conn, &mut transport).await?;
|
||||
let authenticated = client::auth::authenticate(conn, &mut transport).await?;
|
||||
conn.client_id = authenticated.client_id;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,17 +4,24 @@ use arbiter_proto::{
|
||||
google::protobuf::{Empty as ProtoEmpty, Timestamp as ProtoTimestamp},
|
||||
proto::{
|
||||
evm::{
|
||||
EtherTransferSettings as ProtoEtherTransferSettings, EvmError as ProtoEvmError,
|
||||
EvmGrantCreateRequest, EvmGrantCreateResponse, EvmGrantDeleteRequest,
|
||||
EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, GrantEntry,
|
||||
EtherTransferSettings as ProtoEtherTransferSettings,
|
||||
EvalViolation as ProtoEvalViolation, EvmError as ProtoEvmError, EvmGrantCreateRequest,
|
||||
EvmGrantCreateResponse, EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList,
|
||||
EvmGrantListResponse, EvmSignTransactionResponse, GasLimitExceededViolation,
|
||||
GrantEntry, NoMatchingGrantError, PolicyViolationsError,
|
||||
SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant,
|
||||
TokenTransferSettings as ProtoTokenTransferSettings,
|
||||
SpecificMeaning as ProtoSpecificMeaning, TokenInfo as ProtoTokenInfo,
|
||||
TokenTransferSettings as ProtoTokenTransferSettings, TransactionEvalError,
|
||||
TransactionRateLimit as ProtoTransactionRateLimit,
|
||||
VolumeRateLimit as ProtoVolumeRateLimit, WalletCreateResponse, WalletEntry, WalletList,
|
||||
WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult,
|
||||
WalletListResponse, eval_violation::Kind as ProtoEvalViolationKind,
|
||||
evm_grant_create_response::Result as EvmGrantCreateResult,
|
||||
evm_grant_delete_response::Result as EvmGrantDeleteResult,
|
||||
evm_grant_list_response::Result as EvmGrantListResult,
|
||||
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||
specific_grant::Grant as ProtoSpecificGrantType,
|
||||
specific_meaning::Meaning as ProtoSpecificMeaningKind,
|
||||
transaction_eval_error::Kind as ProtoTransactionEvalErrorKind,
|
||||
wallet_create_response::Result as WalletCreateResult,
|
||||
wallet_list_response::Result as WalletListResult,
|
||||
},
|
||||
@@ -23,8 +30,8 @@ use arbiter_proto::{
|
||||
BootstrapResult as ProtoBootstrapResult,
|
||||
SdkClientConnectionResponse as ProtoSdkClientConnectionResponse,
|
||||
UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult,
|
||||
UnsealStart, UserAgentRequest, UserAgentResponse, VaultState as ProtoVaultState,
|
||||
user_agent_request::Payload as UserAgentRequestPayload,
|
||||
UnsealStart, UserAgentEvmSignTransactionRequest, UserAgentRequest, UserAgentResponse,
|
||||
VaultState as ProtoVaultState, user_agent_request::Payload as UserAgentRequestPayload,
|
||||
user_agent_response::Payload as UserAgentResponsePayload,
|
||||
},
|
||||
},
|
||||
@@ -47,7 +54,9 @@ use crate::{
|
||||
session::{
|
||||
BootstrapError, Error, HandleBootstrapEncryptedKey, HandleEvmWalletCreate,
|
||||
HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantList,
|
||||
HandleQueryVaultState, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
||||
HandleQueryVaultState, HandleSignTransaction, HandleUnsealEncryptedKey,
|
||||
HandleUnsealRequest, SignTransactionError as SessionSignTransactionError,
|
||||
UnsealError,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -55,12 +64,124 @@ use crate::{
|
||||
Grant, SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit,
|
||||
ether_transfer, token_transfers,
|
||||
},
|
||||
evm::{PolicyError, VetError, policies::EvalViolation},
|
||||
grpc::request_tracker::RequestTracker,
|
||||
utils::defer,
|
||||
};
|
||||
use alloy::primitives::{Address, U256};
|
||||
use alloy::{
|
||||
consensus::TxEip1559,
|
||||
primitives::{Address, U256},
|
||||
rlp::Decodable,
|
||||
};
|
||||
mod auth;
|
||||
|
||||
fn u256_to_proto_bytes(value: U256) -> Vec<u8> {
|
||||
value.to_be_bytes::<32>().to_vec()
|
||||
}
|
||||
|
||||
fn meaning_to_proto(meaning: crate::evm::policies::SpecificMeaning) -> ProtoSpecificMeaning {
|
||||
let kind = match meaning {
|
||||
crate::evm::policies::SpecificMeaning::EtherTransfer(meaning) => {
|
||||
ProtoSpecificMeaningKind::EtherTransfer(
|
||||
arbiter_proto::proto::evm::EtherTransferMeaning {
|
||||
to: meaning.to.to_vec(),
|
||||
value: u256_to_proto_bytes(meaning.value),
|
||||
},
|
||||
)
|
||||
}
|
||||
crate::evm::policies::SpecificMeaning::TokenTransfer(meaning) => {
|
||||
ProtoSpecificMeaningKind::TokenTransfer(
|
||||
arbiter_proto::proto::evm::TokenTransferMeaning {
|
||||
token: Some(ProtoTokenInfo {
|
||||
symbol: meaning.token.symbol.to_string(),
|
||||
address: meaning.token.contract.to_vec(),
|
||||
chain_id: meaning.token.chain,
|
||||
}),
|
||||
to: meaning.to.to_vec(),
|
||||
value: u256_to_proto_bytes(meaning.value),
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
ProtoSpecificMeaning {
|
||||
meaning: Some(kind),
|
||||
}
|
||||
}
|
||||
|
||||
fn violation_to_proto(violation: EvalViolation) -> ProtoEvalViolation {
|
||||
let kind = match violation {
|
||||
EvalViolation::InvalidTarget { target } => {
|
||||
ProtoEvalViolationKind::InvalidTarget(target.to_vec())
|
||||
}
|
||||
EvalViolation::GasLimitExceeded {
|
||||
max_gas_fee_per_gas,
|
||||
max_priority_fee_per_gas,
|
||||
} => ProtoEvalViolationKind::GasLimitExceeded(GasLimitExceededViolation {
|
||||
max_gas_fee_per_gas: max_gas_fee_per_gas.map(u256_to_proto_bytes),
|
||||
max_priority_fee_per_gas: max_priority_fee_per_gas.map(u256_to_proto_bytes),
|
||||
}),
|
||||
EvalViolation::RateLimitExceeded => {
|
||||
ProtoEvalViolationKind::RateLimitExceeded(ProtoEmpty {})
|
||||
}
|
||||
EvalViolation::VolumetricLimitExceeded => {
|
||||
ProtoEvalViolationKind::VolumetricLimitExceeded(ProtoEmpty {})
|
||||
}
|
||||
EvalViolation::InvalidTime => ProtoEvalViolationKind::InvalidTime(ProtoEmpty {}),
|
||||
EvalViolation::InvalidTransactionType => {
|
||||
ProtoEvalViolationKind::InvalidTransactionType(ProtoEmpty {})
|
||||
}
|
||||
};
|
||||
|
||||
ProtoEvalViolation { kind: Some(kind) }
|
||||
}
|
||||
|
||||
fn eval_error_to_proto(err: VetError) -> Option<TransactionEvalError> {
|
||||
let kind = match err {
|
||||
VetError::ContractCreationNotSupported => {
|
||||
ProtoTransactionEvalErrorKind::ContractCreationNotSupported(ProtoEmpty {})
|
||||
}
|
||||
VetError::UnsupportedTransactionType => {
|
||||
ProtoTransactionEvalErrorKind::UnsupportedTransactionType(ProtoEmpty {})
|
||||
}
|
||||
VetError::Evaluated(meaning, policy_error) => match policy_error {
|
||||
PolicyError::NoMatchingGrant => {
|
||||
ProtoTransactionEvalErrorKind::NoMatchingGrant(NoMatchingGrantError {
|
||||
meaning: Some(meaning_to_proto(meaning)),
|
||||
})
|
||||
}
|
||||
PolicyError::Violations(violations) => {
|
||||
ProtoTransactionEvalErrorKind::PolicyViolations(PolicyViolationsError {
|
||||
meaning: Some(meaning_to_proto(meaning)),
|
||||
violations: violations.into_iter().map(violation_to_proto).collect(),
|
||||
})
|
||||
}
|
||||
PolicyError::Pool(_) | PolicyError::Database(_) => {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Some(TransactionEvalError { kind: Some(kind) })
|
||||
}
|
||||
|
||||
fn decode_eip1559_transaction(payload: &[u8]) -> Result<TxEip1559, ()> {
|
||||
let mut body = payload;
|
||||
if let Some((prefix, rest)) = payload.split_first()
|
||||
&& *prefix == 0x02
|
||||
{
|
||||
body = rest;
|
||||
}
|
||||
|
||||
let mut cursor = body;
|
||||
let transaction = TxEip1559::decode(&mut cursor).map_err(|_| ())?;
|
||||
if !cursor.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
pub struct OutOfBandAdapter(mpsc::Sender<OutOfBand>);
|
||||
|
||||
#[async_trait]
|
||||
@@ -271,6 +392,92 @@ async fn dispatch_conn_message(
|
||||
actor.ask(HandleGrantDelete { grant_id }).await,
|
||||
))
|
||||
}
|
||||
UserAgentRequestPayload::EvmSignTransaction(UserAgentEvmSignTransactionRequest {
|
||||
client_id,
|
||||
request,
|
||||
}) => {
|
||||
if client_id <= 0 {
|
||||
let _ = bi
|
||||
.send(Err(Status::invalid_argument("Invalid SDK client id")))
|
||||
.await;
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let Some(request) = request else {
|
||||
let _ = bi
|
||||
.send(Err(Status::invalid_argument(
|
||||
"Missing EVM sign transaction payload",
|
||||
)))
|
||||
.await;
|
||||
return Err(());
|
||||
};
|
||||
|
||||
let wallet_address = match <[u8; 20]>::try_from(request.wallet_address.as_slice()) {
|
||||
Ok(address) => Address::from(address),
|
||||
Err(_) => {
|
||||
let _ = bi
|
||||
.send(Err(Status::invalid_argument("Invalid EVM wallet address")))
|
||||
.await;
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
|
||||
let transaction = match decode_eip1559_transaction(&request.rlp_transaction) {
|
||||
Ok(transaction) => transaction,
|
||||
Err(()) => {
|
||||
let _ = bi
|
||||
.send(Err(Status::invalid_argument(
|
||||
"Invalid EIP-1559 RLP transaction",
|
||||
)))
|
||||
.await;
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
|
||||
let response = match actor
|
||||
.ask(HandleSignTransaction {
|
||||
client_id,
|
||||
wallet_address,
|
||||
transaction,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(signature) => EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::Signature(
|
||||
signature.as_bytes().to_vec(),
|
||||
)),
|
||||
},
|
||||
Err(SendError::HandlerError(SessionSignTransactionError::Vet(vet_error))) => {
|
||||
match eval_error_to_proto(vet_error) {
|
||||
Some(eval_error) => EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::EvalError(eval_error)),
|
||||
},
|
||||
None => EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::Error(
|
||||
ProtoEvmError::Internal.into(),
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
Err(SendError::HandlerError(SessionSignTransactionError::Internal)) => {
|
||||
EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::Error(
|
||||
ProtoEvmError::Internal.into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(error = ?err, "Failed to sign EVM transaction via user-agent");
|
||||
EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::Error(
|
||||
ProtoEvmError::Internal.into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
UserAgentResponsePayload::EvmSignTransaction(response)
|
||||
}
|
||||
payload => {
|
||||
warn!(?payload, "Unsupported post-auth user agent request");
|
||||
let _ = bi
|
||||
|
||||
Reference in New Issue
Block a user