2 Commits

Author SHA1 Message Date
CleverWild
89e2daf05a ci: fix step name
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-compile Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-04-04 15:22:09 +02:00
CleverWild
c62feda198 ci: add server compile configuration for CI checks on all features
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-compile Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-04-04 15:15:03 +02:00
40 changed files with 240 additions and 216 deletions

View File

@@ -0,0 +1,26 @@
when:
- event: pull_request
path:
include: [".woodpecker/server-*.yaml", "server/**"]
- event: push
branch: main
path:
include: [".woodpecker/server-*.yaml", "server/**"]
steps:
- name: compile
image: jdxcode/mise:latest
directory: server
environment:
CARGO_TERM_COLOR: always
CARGO_TARGET_DIR: /usr/local/cargo/target
CARGO_HOME: /usr/local/cargo/registry
volumes:
- cargo-target:/usr/local/cargo/target
- cargo-registry:/usr/local/cargo/registry
commands:
- apt-get update && apt-get install -y pkg-config
# Install only the necessary Rust toolchain
- mise install rust
- mise install protoc
- cargo check --all-features

View File

@@ -24,4 +24,4 @@ steps:
- mise install rust
- mise install protoc
- mise install cargo:cargo-nextest
- mise exec cargo:cargo-nextest -- cargo nextest run --no-fail-fast --all-features
- mise exec cargo:cargo-nextest -- cargo nextest run --no-fail-fast

2
server/Cargo.lock generated
View File

@@ -724,7 +724,6 @@ name = "arbiter-server"
version = "0.1.0"
dependencies = [
"alloy",
"anyhow",
"arbiter-proto",
"arbiter-tokens-registry",
"argon2",
@@ -742,6 +741,7 @@ dependencies = [
"k256",
"kameo",
"memsafe",
"miette",
"pem",
"prost-types",
"rand 0.10.0",

View File

@@ -22,6 +22,7 @@ chrono = { version = "0.4.44", features = ["serde"] }
rand = "0.10.0"
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
smlang = "0.8.0"
miette = { version = "7.6.0", features = ["fancy", "serde"] }
thiserror = "2.0.18"
async-trait = "0.1.89"
futures = "0.3.32"
@@ -42,4 +43,3 @@ k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
rsa = { version = "0.9", features = ["sha2"] }
sha2 = "0.10"
spki = "0.7"
miette = { version = "7.6.0", features = ["fancy", "serde"] }

View File

@@ -122,7 +122,9 @@ async fn receive_auth_confirmation(
.await
.map_err(|_| AuthError::UnexpectedAuthResponse)?;
let payload = response.payload.ok_or(AuthError::UnexpectedAuthResponse)?;
let payload = response
.payload
.ok_or(AuthError::UnexpectedAuthResponse)?;
match payload {
ClientResponsePayload::Auth(response) => match response.payload {
Some(AuthResponsePayload::Result(result))

View File

@@ -1,3 +1,4 @@
use std::io::{self, Write};
use arbiter_client::ArbiterClient;
@@ -21,6 +22,8 @@ async fn main() {
return;
}
let url = match ArbiterUrl::try_from(input) {
Ok(url) => url,
Err(err) => {
@@ -29,7 +32,7 @@ async fn main() {
}
};
println!("{:#?}", url);
println!("{:#?}", url);
let metadata = ClientMetadata {
name: "arbiter-client test_connect".to_string(),
@@ -41,4 +44,4 @@ async fn main() {
Ok(_) => println!("Connected and authenticated successfully."),
Err(err) => eprintln!("Failed to connect: {:#?}", err),
}
}
}

View File

@@ -1,16 +1,11 @@
use arbiter_proto::{
ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl,
};
use arbiter_proto::{ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl};
use std::sync::Arc;
use tokio::sync::{Mutex, mpsc};
use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::ClientTlsConfig;
use crate::{
StorageError,
auth::{AuthError, authenticate},
storage::{FileSigningKeyStorage, SigningKeyStorage},
transport::{BUFFER_LENGTH, ClientTransport},
StorageError, auth::{AuthError, authenticate}, storage::{FileSigningKeyStorage, SigningKeyStorage}, transport::{BUFFER_LENGTH, ClientTransport}
};
#[cfg(feature = "evm")]
@@ -35,6 +30,7 @@ pub enum Error {
#[error("Storage error")]
Storage(#[from] StorageError),
}
pub struct ArbiterClient {
@@ -65,11 +61,10 @@ impl ArbiterClient {
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
let tls = ClientTlsConfig::new().trust_anchor(anchor);
let channel =
tonic::transport::Channel::from_shared(format!("https://{}:{}", url.host, url.port))?
.tls_config(tls)?
.connect()
.await?;
let channel = tonic::transport::Channel::from_shared(format!("https://{}:{}", url.host, url.port))?
.tls_config(tls)?
.connect()
.await?;
let mut client = ArbiterServiceClient::new(channel);
let (tx, rx) = mpsc::channel(BUFFER_LENGTH);

View File

@@ -9,4 +9,4 @@ pub use client::{ArbiterClient, Error};
pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
#[cfg(feature = "evm")]
pub use wallets::evm::{ArbiterEvmSignTransactionError, ArbiterEvmWallet};
pub use wallets::evm::ArbiterEvmWallet;

View File

@@ -10,48 +10,14 @@ use tokio::sync::Mutex;
use arbiter_proto::proto::{
client::{
ClientRequest,
client_request::Payload as ClientRequestPayload,
ClientRequest, client_request::Payload as ClientRequestPayload,
client_response::Payload as ClientResponsePayload,
evm::{
self as proto_evm, request::Payload as EvmRequestPayload,
response::Payload as EvmResponsePayload,
},
},
evm::{
EvmSignTransactionRequest,
evm_sign_transaction_response::Result as EvmSignTransactionResult,
},
shared::evm::TransactionEvalError,
evm::evm_sign_transaction_response::Result as EvmSignTransactionResult,
};
use crate::transport::{ClientTransport, next_request_id};
/// A typed error payload returned by [`ArbiterEvmWallet`] transaction signing.
///
/// This is wrapped into `alloy::signers::Error::Other`, so consumers can downcast by [`TryFrom`] and
/// interpret the concrete policy evaluation failure instead of parsing strings.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ArbiterEvmSignTransactionError {
#[error("transaction rejected by policy: {0:?}")]
PolicyEval(TransactionEvalError),
}
impl<'a> TryFrom<&'a Error> for &'a ArbiterEvmSignTransactionError {
type Error = ();
fn try_from(value: &'a Error) -> Result<Self, Self::Error> {
if let Error::Other(inner) = value
&& let Some(eval_error) = inner.downcast_ref()
{
Ok(eval_error)
} else {
Err(())
}
}
}
pub struct ArbiterEvmWallet {
transport: Arc<Mutex<ClientTransport>>,
address: Address,
@@ -130,14 +96,12 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
transport
.send(ClientRequest {
request_id,
payload: Some(ClientRequestPayload::Evm(proto_evm::Request {
payload: Some(EvmRequestPayload::SignTransaction(
EvmSignTransactionRequest {
wallet_address: self.address.to_vec(),
rlp_transaction,
},
)),
})),
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"))?;
@@ -157,21 +121,12 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
.payload
.ok_or_else(|| Error::other("missing evm sign transaction response payload"))?;
let ClientResponsePayload::Evm(proto_evm::Response {
payload: Some(payload),
}) = payload
else {
let ClientResponsePayload::EvmSignTransaction(response) = payload else {
return Err(Error::other(
"unexpected response payload for evm sign transaction request",
));
};
let EvmResponsePayload::SignTransaction(response) = payload else {
return Err(Error::other(
"unexpected evm response payload for sign transaction request",
));
};
let result = response
.result
.ok_or_else(|| Error::other("missing evm sign transaction result"))?;
@@ -181,9 +136,9 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
Signature::try_from(signature.as_slice())
.map_err(|_| Error::other("invalid signature returned by server"))
}
EvmSignTransactionResult::EvalError(eval_error) => Err(Error::other(
ArbiterEvmSignTransactionError::PolicyEval(eval_error),
)),
EvmSignTransactionResult::EvalError(eval_error) => Err(Error::other(format!(
"transaction rejected by policy: {eval_error:?}"
))),
EvmSignTransactionResult::Error(code) => Err(Error::other(format!(
"server failed to sign transaction with error code {code}"
))),

View File

@@ -7,6 +7,7 @@ const ARBITER_URL_SCHEME: &str = "arbiter";
const CERT_QUERY_KEY: &str = "cert";
const BOOTSTRAP_TOKEN_QUERY_KEY: &str = "bootstrap_token";
#[derive(Debug, Clone)]
pub struct ArbiterUrl {
pub host: String,

View File

@@ -25,6 +25,7 @@ tonic.features = ["tls-aws-lc"]
tokio.workspace = true
rustls.workspace = true
smlang.workspace = true
miette.workspace = true
thiserror.workspace = true
fatality = "0.1.1"
diesel_migrations = { version = "2.3.1", features = ["sqlite"] }
@@ -52,7 +53,6 @@ spki.workspace = true
alloy.workspace = true
prost-types.workspace = true
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
anyhow = "1.0.102"
[dev-dependencies]
insta = "1.46.3"

View File

@@ -2,7 +2,7 @@ use arbiter_proto::{BOOTSTRAP_PATH, home_path};
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use kameo::{Actor, messages};
use miette::Diagnostic;
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
use thiserror::Error;
@@ -25,15 +25,18 @@ pub async fn generate_token() -> Result<String, std::io::Error> {
Ok(token)
}
#[derive(Error, Debug)]
#[derive(Error, Debug, Diagnostic)]
pub enum Error {
#[error("Database error: {0}")]
#[diagnostic(code(arbiter_server::bootstrap::database))]
Database(#[from] db::PoolError),
#[error("Database query error: {0}")]
#[diagnostic(code(arbiter_server::bootstrap::database_query))]
Query(#[from] diesel::result::Error),
#[error("I/O error: {0}")]
#[diagnostic(code(arbiter_server::bootstrap::io))]
Io(#[from] std::io::Error),
}

View File

@@ -287,7 +287,10 @@ where
Ok(())
}
pub async fn authenticate<T>(props: &mut ClientConnection, transport: &mut T) -> Result<i32, Error>
pub async fn authenticate<T>(
props: &mut ClientConnection,
transport: &mut T,
) -> Result<i32, Error>
where
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
{
@@ -316,7 +319,7 @@ where
sync_client_metadata(&props.db, info.id, &metadata).await?;
challenge_client(transport, pubkey, info.current_nonce).await?;
transport
.send(Ok(Outbound::AuthSuccess))
.await

View File

@@ -20,7 +20,10 @@ pub struct ClientConnection {
impl ClientConnection {
pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self {
Self { db, actors }
Self {
db,
actors,
}
}
}

View File

@@ -6,10 +6,11 @@ use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
use crate::{
actors::{
GlobalActors,
client::ClientConnection,
client::ClientConnection, flow_coordinator::RegisterClient,
evm::{ClientSignTransaction, SignTransactionError},
flow_coordinator::RegisterClient,
keyholder::KeyHolderState,
},
db,
evm::VetError,
@@ -94,10 +95,7 @@ impl Actor for ClientSession {
impl ClientSession {
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
let props = ClientConnection::new(db, actors);
Self {
props,
client_id: 0,
}
Self { props, client_id: 0 }
}
}

View File

@@ -25,36 +25,45 @@ use crate::{
pub use crate::evm::safe_signer;
#[derive(Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum SignTransactionError {
#[error("Wallet not found")]
#[diagnostic(code(arbiter::evm::sign::wallet_not_found))]
WalletNotFound,
#[error("Database error: {0}")]
#[diagnostic(code(arbiter::evm::sign::database))]
Database(#[from] DatabaseError),
#[error("Keyholder error: {0}")]
#[diagnostic(code(arbiter::evm::sign::keyholder))]
Keyholder(#[from] crate::actors::keyholder::Error),
#[error("Keyholder mailbox error")]
#[diagnostic(code(arbiter::evm::sign::keyholder_send))]
KeyholderSend,
#[error("Signing error: {0}")]
#[diagnostic(code(arbiter::evm::sign::signing))]
Signing(#[from] alloy::signers::Error),
#[error("Policy error: {0}")]
#[diagnostic(code(arbiter::evm::sign::vet))]
Vet(#[from] evm::VetError),
}
#[derive(Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum Error {
#[error("Keyholder error: {0}")]
#[diagnostic(code(arbiter::evm::keyholder))]
Keyholder(#[from] crate::actors::keyholder::Error),
#[error("Keyholder mailbox error")]
#[diagnostic(code(arbiter::evm::keyholder_send))]
KeyholderSend,
#[error("Database error: {0}")]
#[diagnostic(code(arbiter::evm::database))]
Database(#[from] DatabaseError),
}

View File

@@ -15,7 +15,7 @@ use crate::actors::{
pub struct Args {
pub client: ClientProfile,
pub user_agents: Vec<ActorRef<UserAgentSession>>,
pub reply: ReplySender<Result<bool, ApprovalError>>,
pub reply: ReplySender<Result<bool, ApprovalError>>
}
pub struct ClientApprovalController {
@@ -39,11 +39,7 @@ impl Actor for ClientApprovalController {
type Error = ();
async fn on_start(
Args {
client,
mut user_agents,
reply,
}: Self::Args,
Args { client, mut user_agents, reply }: Self::Args,
actor_ref: ActorRef<Self>,
) -> Result<Self, Self::Error> {
let this = Self {

View File

@@ -35,28 +35,36 @@ enum State {
},
}
#[derive(Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum Error {
#[error("Keyholder is already bootstrapped")]
#[diagnostic(code(arbiter::keyholder::already_bootstrapped))]
AlreadyBootstrapped,
#[error("Keyholder is not bootstrapped")]
#[diagnostic(code(arbiter::keyholder::not_bootstrapped))]
NotBootstrapped,
#[error("Invalid key provided")]
#[diagnostic(code(arbiter::keyholder::invalid_key))]
InvalidKey,
#[error("Requested aead entry not found")]
#[diagnostic(code(arbiter::keyholder::aead_not_found))]
NotFound,
#[error("Encryption error: {0}")]
#[diagnostic(code(arbiter::keyholder::encryption_error))]
Encryption(#[from] chacha20poly1305::aead::Error),
#[error("Database error: {0}")]
#[diagnostic(code(arbiter::keyholder::database_error))]
DatabaseConnection(#[from] db::PoolError),
#[error("Database transaction error: {0}")]
#[diagnostic(code(arbiter::keyholder::database_transaction_error))]
DatabaseTransaction(#[from] diesel::result::Error),
#[error("Broken database")]
#[diagnostic(code(arbiter::keyholder::broken_database))]
BrokenDatabase,
}

View File

@@ -1,4 +1,5 @@
use kameo::actor::{ActorRef, Spawn};
use miette::Diagnostic;
use thiserror::Error;
use crate::{
@@ -16,12 +17,14 @@ pub mod flow_coordinator;
pub mod keyholder;
pub mod user_agent;
#[derive(Error, Debug)]
#[derive(Error, Debug, Diagnostic)]
pub enum SpawnError {
#[error("Failed to spawn Bootstrapper actor")]
#[diagnostic(code(SpawnError::Bootstrapper))]
Bootstrapper(#[from] bootstrap::Error),
#[error("Failed to spawn KeyHolder actor")]
#[diagnostic(code(SpawnError::KeyHolder))]
KeyHolder(#[from] keyholder::Error),
}

View File

@@ -5,8 +5,8 @@ use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::error::SendError;
use kameo::messages;
use kameo::prelude::Context;
use kameo::messages;
use tracing::{error, info};
use x25519_dalek::{EphemeralSecret, PublicKey};

View File

@@ -1,5 +1,6 @@
use std::sync::Arc;
use miette::Diagnostic;
use thiserror::Error;
use crate::{
@@ -10,24 +11,30 @@ use crate::{
pub mod tls;
#[derive(Error, Debug)]
#[derive(Error, Debug, Diagnostic)]
pub enum InitError {
#[error("Database setup failed: {0}")]
#[diagnostic(code(arbiter_server::init::database_setup))]
DatabaseSetup(#[from] db::DatabaseSetupError),
#[error("Connection acquire failed: {0}")]
#[diagnostic(code(arbiter_server::init::database_pool))]
DatabasePool(#[from] db::PoolError),
#[error("Database query error: {0}")]
#[diagnostic(code(arbiter_server::init::database_query))]
DatabaseQuery(#[from] diesel::result::Error),
#[error("TLS initialization failed: {0}")]
#[diagnostic(code(arbiter_server::init::tls_init))]
Tls(#[from] tls::InitError),
#[error("Actor spawn failed: {0}")]
#[diagnostic(code(arbiter_server::init::actor_spawn))]
ActorSpawn(#[from] crate::actors::SpawnError),
#[error("I/O Error: {0}")]
#[diagnostic(code(arbiter_server::init::io))]
Io(#[from] std::io::Error),
}

View File

@@ -1,8 +1,8 @@
use std::{net::{IpAddr, Ipv4Addr}, string::FromUtf8Error};
use std::{net::IpAddr, string::FromUtf8Error};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _};
use diesel_async::{AsyncConnection, RunQueryDsl};
use miette::Diagnostic;
use pem::Pem;
use rcgen::{
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
@@ -29,24 +29,30 @@ const ENCODE_CONFIG: pem::EncodeConfig = {
pem::EncodeConfig::new().set_line_ending(line_ending)
};
#[derive(Error, Debug)]
#[derive(Error, Debug, Diagnostic)]
pub enum InitError {
#[error("Key generation error during TLS initialization: {0}")]
#[diagnostic(code(arbiter_server::tls_init::key_generation))]
KeyGeneration(#[from] rcgen::Error),
#[error("Key invalid format: {0}")]
#[diagnostic(code(arbiter_server::tls_init::key_invalid_format))]
KeyInvalidFormat(#[from] FromUtf8Error),
#[error("Key deserialization error: {0}")]
#[diagnostic(code(arbiter_server::tls_init::key_deserialization))]
KeyDeserializationError(rcgen::Error),
#[error("Database error during TLS initialization: {0}")]
#[diagnostic(code(arbiter_server::tls_init::database_error))]
DatabaseError(#[from] diesel::result::Error),
#[error("Pem deserialization error during TLS initialization: {0}")]
#[diagnostic(code(arbiter_server::tls_init::pem_deserialization))]
PemDeserializationError(#[from] rustls::pki_types::pem::Error),
#[error("Database pool acquire error during TLS initialization: {0}")]
#[diagnostic(code(arbiter_server::tls_init::database_pool_acquire))]
DatabasePoolAcquire(#[from] db::PoolError),
}
@@ -110,7 +116,9 @@ impl TlsCa {
];
params
.subject_alt_names
.push(SanType::IpAddress(Ipv4Addr::LOCALHOST.into()));
.push(SanType::IpAddress(IpAddr::from([
127, 0, 0, 1,
])));
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "Arbiter Instance Leaf");

View File

@@ -5,7 +5,7 @@ use diesel_async::{
sync_connection_wrapper::SyncConnectionWrapper,
};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
use miette::Diagnostic;
use thiserror::Error;
use tracing::info;
@@ -21,21 +21,26 @@ static DB_FILE: &str = "arbiter.sqlite";
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
#[derive(Error, Debug)]
#[derive(Error, Diagnostic, Debug)]
pub enum DatabaseSetupError {
#[error("Failed to determine home directory")]
#[diagnostic(code(arbiter::db::home_dir))]
HomeDir(std::io::Error),
#[error(transparent)]
#[diagnostic(code(arbiter::db::connection))]
Connection(diesel::ConnectionError),
#[error(transparent)]
#[diagnostic(code(arbiter::db::concurrency))]
ConcurrencySetup(diesel::result::Error),
#[error(transparent)]
#[diagnostic(code(arbiter::db::migration))]
Migration(Box<dyn std::error::Error + Send + Sync>),
#[error(transparent)]
#[diagnostic(code(arbiter::db::pool))]
Pool(#[from] PoolInitError),
}

View File

@@ -28,32 +28,39 @@ pub mod policies;
mod utils;
/// Errors that can only occur once the transaction meaning is known (during policy evaluation)
#[derive(Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum PolicyError {
#[error("Database error")]
Database(#[from] crate::db::DatabaseError),
#[error("Transaction violates policy: {0:?}")]
#[diagnostic(code(arbiter_server::evm::policy_error::violation))]
Violations(Vec<EvalViolation>),
#[error("No matching grant found")]
#[diagnostic(code(arbiter_server::evm::policy_error::no_matching_grant))]
NoMatchingGrant,
}
#[derive(Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum VetError {
#[error("Contract creation transactions are not supported")]
#[diagnostic(code(arbiter_server::evm::vet_error::contract_creation_unsupported))]
ContractCreationNotSupported,
#[error("Engine can't classify this transaction")]
#[diagnostic(code(arbiter_server::evm::vet_error::unsupported))]
UnsupportedTransactionType,
#[error("Policy evaluation failed: {1}")]
#[diagnostic(code(arbiter_server::evm::vet_error::evaluated))]
Evaluated(SpecificMeaning, #[source] PolicyError),
}
#[derive(Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum AnalyzeError {
#[error("Engine doesn't support granting permissions for contract creation")]
#[diagnostic(code(arbiter_server::evm::analyze_error::contract_creation_not_supported))]
ContractCreationNotSupported,
#[error("Unsupported transaction type")]
#[diagnostic(code(arbiter_server::evm::analyze_error::unsupported_transaction_type))]
UnsupportedTransactionType,
}

View File

@@ -6,7 +6,7 @@ use diesel::{
ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite,
};
use diesel_async::{AsyncConnection, RunQueryDsl};
use miette::Diagnostic;
use thiserror::Error;
use crate::{
@@ -33,27 +33,33 @@ pub struct EvalContext {
pub max_priority_fee_per_gas: u128,
}
#[derive(Debug, Error)]
#[derive(Debug, Error, Diagnostic)]
pub enum EvalViolation {
#[error("This grant doesn't allow transactions to the target address {target}")]
#[diagnostic(code(arbiter_server::evm::eval_violation::invalid_target))]
InvalidTarget { target: Address },
#[error("Gas limit exceeded for this grant")]
#[diagnostic(code(arbiter_server::evm::eval_violation::gas_limit_exceeded))]
GasLimitExceeded {
max_gas_fee_per_gas: Option<U256>,
max_priority_fee_per_gas: Option<U256>,
},
#[error("Rate limit exceeded for this grant")]
#[diagnostic(code(arbiter_server::evm::eval_violation::rate_limit_exceeded))]
RateLimitExceeded,
#[error("Transaction exceeds volumetric limits of the grant")]
#[diagnostic(code(arbiter_server::evm::eval_violation::volumetric_limit_exceeded))]
VolumetricLimitExceeded,
#[error("Transaction is outside of the grant's validity period")]
#[diagnostic(code(arbiter_server::evm::eval_violation::invalid_time))]
InvalidTime,
#[error("Transaction type is not allowed by this grant")]
#[diagnostic(code(arbiter_server::evm::eval_violation::invalid_transaction_type))]
InvalidTransactionType,
}

View File

@@ -140,9 +140,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
let Some(payload) = auth_request.payload else {
let _ = self
.bi
.send(Err(Status::invalid_argument(
"Missing client auth request payload",
)))
.send(Err(Status::invalid_argument("Missing client auth request payload")))
.await;
return None;
};
@@ -172,7 +170,9 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
metadata: client_metadata_from_proto(client_info),
})
}
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution {
signature,
}) => {
let Ok(signature) = ed25519_dalek::Signature::try_from(signature.as_slice()) else {
let _ = self
.send_auth_result(ProtoAuthResult::InvalidSignature)

View File

@@ -34,9 +34,7 @@ pub(super) async fn dispatch(
req: proto_evm::Request,
) -> Result<ClientResponsePayload, Status> {
let Some(payload) = req.payload else {
return Err(Status::invalid_argument(
"Missing client EVM request payload",
));
return Err(Status::invalid_argument("Missing client EVM request payload"));
};
match payload {
@@ -61,13 +59,13 @@ pub(super) async fn dispatch(
))) => EvmSignTransactionResponse {
result: Some(vet_error.convert()),
},
Err(kameo::error::SendError::HandlerError(SignTransactionRpcError::Internal)) => {
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 {
@@ -80,8 +78,8 @@ pub(super) async fn dispatch(
Ok(wrap_response(EvmResponsePayload::SignTransaction(response)))
}
EvmRequestPayload::AnalyzeTransaction(_) => Err(Status::unimplemented(
"EVM transaction analysis is not yet implemented",
)),
EvmRequestPayload::AnalyzeTransaction(_) => {
Err(Status::unimplemented("EVM transaction analysis is not yet implemented"))
}
}
}

View File

@@ -12,9 +12,11 @@ use kameo::{actor::ActorRef, error::SendError};
use tonic::Status;
use tracing::warn;
use crate::actors::{
client::session::{ClientSession, Error, HandleQueryVaultState},
keyholder::KeyHolderState,
use crate::{
actors::{
client::session::{ClientSession, Error, HandleQueryVaultState},
keyholder::KeyHolderState,
},
};
pub(super) async fn dispatch(
@@ -22,9 +24,7 @@ pub(super) async fn dispatch(
req: proto_vault::Request,
) -> Result<ClientResponsePayload, Status> {
let Some(payload) = req.payload else {
return Err(Status::invalid_argument(
"Missing client vault request payload",
));
return Err(Status::invalid_argument("Missing client vault request payload"));
};
match payload {

View File

@@ -28,8 +28,9 @@ impl TryConvert for RawEvmTransaction {
type Error = tonic::Status;
fn try_convert(self) -> Result<Self::Output, Self::Error> {
let tx = TxEip1559::decode(&mut self.0.as_slice())
.map_err(|_| tonic::Status::invalid_argument("Invalid EVM transaction format"))?;
let tx = TxEip1559::decode(&mut self.0.as_slice()).map_err(|_| {
tonic::Status::invalid_argument("Invalid EVM transaction format")
})?;
Ok(tx)
}
}
}

View File

@@ -1,12 +1,9 @@
use alloy::primitives::U256;
use arbiter_proto::proto::{
evm::{
EvmError as ProtoEvmError,
evm_sign_transaction_response::Result as EvmSignTransactionResult,
},
evm::{EvmError as ProtoEvmError, evm_sign_transaction_response::Result as EvmSignTransactionResult},
shared::evm::{
EvalViolation as ProtoEvalViolation, GasLimitExceededViolation, NoMatchingGrantError,
PolicyViolationsError, SpecificMeaning as ProtoSpecificMeaning,
EvalViolation as ProtoEvalViolation, GasLimitExceededViolation,
NoMatchingGrantError, PolicyViolationsError, SpecificMeaning as ProtoSpecificMeaning,
TokenInfo as ProtoTokenInfo, TransactionEvalError as ProtoTransactionEvalError,
eval_violation::Kind as ProtoEvalViolationKind,
specific_meaning::Meaning as ProtoSpecificMeaningKind,

View File

@@ -1,10 +1,12 @@
use tokio::sync::mpsc;
use arbiter_proto::{
proto::user_agent::{
UserAgentRequest, UserAgentResponse,
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
proto::{
user_agent::{
UserAgentRequest, UserAgentResponse,
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
},
},
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
};

View File

@@ -1,14 +1,12 @@
use arbiter_proto::{
proto::user_agent::{
UserAgentRequest, UserAgentResponse,
auth::{
UserAgentRequest, UserAgentResponse, auth::{
self as proto_auth, AuthChallenge as ProtoAuthChallenge,
AuthChallengeRequest as ProtoAuthChallengeRequest,
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
KeyType as ProtoKeyType, request::Payload as AuthRequestPayload,
response::Payload as AuthResponsePayload,
},
user_agent_request::Payload as UserAgentRequestPayload,
}, user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
},
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
@@ -65,9 +63,7 @@ impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
Ok(Outbound::AuthChallenge { nonce }) => {
AuthResponsePayload::Challenge(ProtoAuthChallenge { nonce })
}
Ok(Outbound::AuthSuccess) => {
AuthResponsePayload::Result(ProtoAuthResult::Success.into())
}
Ok(Outbound::AuthSuccess) => AuthResponsePayload::Result(ProtoAuthResult::Success.into()),
Err(Error::UnregisteredPublicKey) => {
AuthResponsePayload::Result(ProtoAuthResult::InvalidKey.into())
}
@@ -175,9 +171,9 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
bootstrap_token,
})
}
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
Some(auth::Inbound::AuthChallengeSolution { signature })
}
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution {
signature,
}) => Some(auth::Inbound::AuthChallengeSolution { signature }),
}
}
}

View File

@@ -3,7 +3,8 @@ use arbiter_proto::proto::{
EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse,
EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse,
EvmSignTransactionResponse, GrantEntry, WalletCreateResponse, WalletEntry, WalletList,
WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult,
WalletListResponse,
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,
@@ -164,12 +165,7 @@ async fn handle_grant_delete(
actor: &ActorRef<UserAgentSession>,
req: EvmGrantDeleteRequest,
) -> Result<Option<UserAgentResponsePayload>, Status> {
let result = match actor
.ask(HandleGrantDelete {
grant_id: req.grant_id,
})
.await
{
let result = match actor.ask(HandleGrantDelete { grant_id: req.grant_id }).await {
Ok(()) => EvmGrantDeleteResult::Ok(()),
Err(err) => {
warn!(error = ?err, "Failed to delete EVM grant");
@@ -206,18 +202,18 @@ async fn handle_sign_transaction(
signature.as_bytes().to_vec(),
)),
},
Err(kameo::error::SendError::HandlerError(SessionSignTransactionError::Vet(vet_error))) => {
EvmSignTransactionResponse {
result: Some(vet_error.convert()),
}
}
Err(kameo::error::SendError::HandlerError(SessionSignTransactionError::Internal)) => {
EvmSignTransactionResponse {
result: Some(EvmSignTransactionResult::Error(
ProtoEvmError::Internal.into(),
)),
}
}
Err(kameo::error::SendError::HandlerError(
SessionSignTransactionError::Vet(vet_error),
)) => EvmSignTransactionResponse {
result: Some(vet_error.convert()),
},
Err(kameo::error::SendError::HandlerError(
SessionSignTransactionError::Internal,
)) => EvmSignTransactionResponse {
result: Some(EvmSignTransactionResult::Error(
ProtoEvmError::Internal.into(),
)),
},
Err(err) => {
warn!(error = ?err, "Failed to sign EVM transaction");
EvmSignTransactionResponse {
@@ -228,7 +224,7 @@ async fn handle_sign_transaction(
}
};
Ok(Some(wrap_evm_response(
EvmResponsePayload::SignTransaction(response),
)))
Ok(Some(wrap_evm_response(EvmResponsePayload::SignTransaction(
response,
))))
}

View File

@@ -5,7 +5,9 @@ use arbiter_proto::proto::{
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
specific_grant::Grant as ProtoSpecificGrantType,
},
user_agent::sdk_client::{WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess},
user_agent::sdk_client::{
WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess,
},
};
use chrono::{DateTime, Utc};
use prost_types::Timestamp as ProtoTimestamp;

View File

@@ -1,5 +1,4 @@
use arbiter_proto::proto::{
shared::ClientInfo as ProtoClientMetadata,
user_agent::{
sdk_client::{
self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel,
@@ -14,6 +13,7 @@ use arbiter_proto::proto::{
},
user_agent_response::Payload as UserAgentResponsePayload,
},
shared::ClientInfo as ProtoClientMetadata,
};
use kameo::actor::ActorRef;
use tonic::Status;
@@ -62,22 +62,18 @@ pub(super) async fn dispatch(
req: proto_sdk_client::Request,
) -> Result<Option<UserAgentResponsePayload>, Status> {
let Some(payload) = req.payload else {
return Err(Status::invalid_argument(
"Missing SDK client request payload",
));
return Err(Status::invalid_argument("Missing SDK client request payload"));
};
match payload {
SdkClientRequestPayload::ConnectionResponse(resp) => {
handle_connection_response(actor, resp).await
}
SdkClientRequestPayload::Revoke(_) => Err(Status::unimplemented(
"SdkClientRevoke is not yet implemented",
)),
SdkClientRequestPayload::List(_) => handle_list(actor).await,
SdkClientRequestPayload::GrantWalletAccess(req) => {
handle_grant_wallet_access(actor, req).await
SdkClientRequestPayload::Revoke(_) => {
Err(Status::unimplemented("SdkClientRevoke is not yet implemented"))
}
SdkClientRequestPayload::List(_) => handle_list(actor).await,
SdkClientRequestPayload::GrantWalletAccess(req) => handle_grant_wallet_access(actor, req).await,
SdkClientRequestPayload::RevokeWalletAccess(req) => {
handle_revoke_wallet_access(actor, req).await
}
@@ -132,11 +128,11 @@ async fn handle_list(
ProtoSdkClientListResult::Error(ProtoSdkClientError::Internal.into())
}
};
Ok(Some(wrap_sdk_client_response(
SdkClientResponsePayload::List(ProtoSdkClientListResponse {
Ok(Some(wrap_sdk_client_response(SdkClientResponsePayload::List(
ProtoSdkClientListResponse {
result: Some(result),
}),
)))
},
))))
}
async fn handle_grant_wallet_access(

View File

@@ -1,4 +1,3 @@
use arbiter_proto::proto::shared::VaultState as ProtoVaultState;
use arbiter_proto::proto::user_agent::{
user_agent_response::Payload as UserAgentResponsePayload,
vault::{
@@ -12,21 +11,25 @@ use arbiter_proto::proto::user_agent::{
unseal::{
self as proto_unseal, UnsealEncryptedKey as ProtoUnsealEncryptedKey,
UnsealResult as ProtoUnsealResult, UnsealStart,
request::Payload as UnsealRequestPayload, response::Payload as UnsealResponsePayload,
request::Payload as UnsealRequestPayload,
response::Payload as UnsealResponsePayload,
},
},
};
use arbiter_proto::proto::shared::VaultState as ProtoVaultState;
use kameo::{actor::ActorRef, error::SendError};
use tonic::Status;
use tracing::warn;
use crate::actors::{
keyholder::KeyHolderState,
user_agent::{
UserAgentSession,
session::connection::{
BootstrapError, HandleBootstrapEncryptedKey, HandleQueryVaultState,
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
use crate::{
actors::{
keyholder::KeyHolderState,
user_agent::{
UserAgentSession,
session::connection::{
BootstrapError, HandleBootstrapEncryptedKey, HandleQueryVaultState,
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
},
},
},
};
@@ -148,9 +151,7 @@ async fn handle_bootstrap_encrypted_key(
.await
{
Ok(()) => ProtoBootstrapResult::Success,
Err(SendError::HandlerError(BootstrapError::InvalidKey)) => {
ProtoBootstrapResult::InvalidKey
}
Err(SendError::HandlerError(BootstrapError::InvalidKey)) => ProtoBootstrapResult::InvalidKey,
Err(SendError::HandlerError(BootstrapError::AlreadyBootstrapped)) => {
ProtoBootstrapResult::AlreadyBootstrapped
}

View File

@@ -1,8 +1,8 @@
use std::net::SocketAddr;
use anyhow::anyhow;
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
use arbiter_server::{Server, actors::bootstrap::GetToken, context::ServerContext, db};
use miette::miette;
use rustls::crypto::aws_lc_rs;
use tonic::transport::{Identity, ServerTlsConfig};
use tracing::info;
@@ -10,7 +10,7 @@ use tracing::info;
const PORT: u16 = 50051;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
async fn main() -> miette::Result<()> {
aws_lc_rs::default_provider().install_default().unwrap();
tracing_subscriber::fmt()
@@ -46,11 +46,11 @@ async fn main() -> anyhow::Result<()> {
tonic::transport::Server::builder()
.tls_config(tls)
.map_err(|err| anyhow!("Failed to setup TLS: {err}"))?
.map_err(|err| miette!("Faild to setup TLS: {err}"))?
.add_service(ArbiterServiceServer::new(Server::new(context)))
.serve(addr)
.await
.map_err(|e| anyhow!("gRPC server error: {e}"))?;
.map_err(|e| miette::miette!("gRPC server error: {e}"))?;
unreachable!("gRPC server should run indefinitely");
}

View File

@@ -2,10 +2,9 @@ use arbiter_server::{
actors::{
GlobalActors,
keyholder::{Bootstrap, Seal},
user_agent::{
UserAgentSession,
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
},
user_agent::{UserAgentSession, session::connection::{
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
}},
},
db,
safe_cell::{SafeCell, SafeCellHandle as _},