From cfa6e068ebfa32e25e17a18a9c3bca709fd95274 Mon Sep 17 00:00:00 2001 From: hdbg Date: Thu, 19 Mar 2026 09:03:22 +0100 Subject: [PATCH] feat(client): add client metadata and wallet visibility support --- protobufs/client.proto | 7 + protobufs/evm.proto | 11 +- protobufs/user_agent.proto | 2 + server/crates/arbiter-proto/src/transport.rs | 2 +- .../2026-02-14-171124-0000_init/up.sql | 117 +++++---- .../arbiter-server/src/actors/client/auth.rs | 152 ++++++++++- .../arbiter-server/src/actors/evm/mod.rs | 53 ++-- .../src/actors/user_agent/mod.rs | 1 - .../src/actors/user_agent/session.rs | 7 +- .../actors/user_agent/session/connection.rs | 13 +- server/crates/arbiter-server/src/db/models.rs | 39 ++- server/crates/arbiter-server/src/db/schema.rs | 50 +++- server/crates/arbiter-server/src/evm/mod.rs | 20 +- .../crates/arbiter-server/src/evm/policies.rs | 13 +- .../src/evm/policies/ether_transfer/mod.rs | 5 +- .../src/evm/policies/ether_transfer/tests.rs | 30 +-- .../src/evm/policies/token_transfers/mod.rs | 3 +- .../src/evm/policies/token_transfers/tests.rs | 19 +- .../crates/arbiter-server/src/grpc/client.rs | 9 +- .../arbiter-server/src/grpc/client/auth.rs | 46 ++-- .../src/grpc/request_tracker.rs | 6 + .../arbiter-server/src/grpc/user_agent.rs | 31 +-- .../src/grpc/user_agent/auth.rs | 22 +- server/crates/arbiter-server/src/lib.rs | 3 - .../arbiter-server/tests/client/auth.rs | 240 +++++++++++++++++- .../arbiter-tokens-registry/src/evm/mod.rs | 2 +- .../crates/arbiter-tokens-registry/src/lib.rs | 2 +- 27 files changed, 669 insertions(+), 236 deletions(-) diff --git a/protobufs/client.proto b/protobufs/client.proto index c090a0d..1ceac34 100644 --- a/protobufs/client.proto +++ b/protobufs/client.proto @@ -5,8 +5,15 @@ package arbiter.client; import "evm.proto"; import "google/protobuf/empty.proto"; +message ClientInfo { + string name = 1; + string description = 2; + string version = 3; +} + message AuthChallengeRequest { bytes pubkey = 1; + ClientInfo client_info = 2; } message AuthChallenge { diff --git a/protobufs/evm.proto b/protobufs/evm.proto index b6469dd..15f0994 100644 --- a/protobufs/evm.proto +++ b/protobufs/evm.proto @@ -46,7 +46,7 @@ message VolumeRateLimit { } message SharedSettings { - int32 wallet_id = 1; + int32 visibility_id = 1; uint64 chain_id = 2; optional google.protobuf.Timestamp valid_from = 3; optional google.protobuf.Timestamp valid_until = 4; @@ -139,9 +139,8 @@ message TransactionEvalError { // --- UserAgent grant management --- message EvmGrantCreateRequest { - int32 client_id = 1; - SharedSettings shared = 2; - SpecificGrant specific = 3; + SharedSettings shared = 1; + SpecificGrant specific = 2; } message EvmGrantCreateResponse { @@ -165,13 +164,13 @@ message EvmGrantDeleteResponse { // Basic grant info returned in grant listings message GrantEntry { int32 id = 1; - int32 client_id = 2; + int32 visibility_id = 2; SharedSettings shared = 3; SpecificGrant specific = 4; } message EvmGrantListRequest { - optional int32 wallet_id = 1; + optional int32 visibility_id = 1; } message EvmGrantListResponse { diff --git a/protobufs/user_agent.proto b/protobufs/user_agent.proto index f54f05a..19f9705 100644 --- a/protobufs/user_agent.proto +++ b/protobufs/user_agent.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package arbiter.user_agent; +import "client.proto"; import "evm.proto"; import "google/protobuf/empty.proto"; @@ -80,6 +81,7 @@ enum VaultState { message ClientConnectionRequest { bytes pubkey = 1; + arbiter.client.ClientInfo info = 2; } message ClientConnectionResponse { diff --git a/server/crates/arbiter-proto/src/transport.rs b/server/crates/arbiter-proto/src/transport.rs index 25259c3..5f8b5b4 100644 --- a/server/crates/arbiter-proto/src/transport.rs +++ b/server/crates/arbiter-proto/src/transport.rs @@ -89,7 +89,7 @@ pub trait Sender: Send + Sync { } #[async_trait] -pub trait Receiver: Send + Sync { +pub trait Receiver: Send + Sync { async fn recv(&mut self) -> Option; } diff --git a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql index 3419ef9..b3c83e6 100644 --- a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql +++ b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql @@ -28,7 +28,7 @@ create table if not exists tls_history ( id INTEGER not null PRIMARY KEY, cert text not null, cert_key text not null, -- PEM Encoded private key - ca_cert text not null, + ca_cert text not null, ca_key text not null, -- PEM Encoded private key created_at integer not null default(unixepoch ('now')) ) STRICT; @@ -40,7 +40,8 @@ create table if not exists arbiter_settings ( tls_id integer references tls_history (id) on delete RESTRICT ) STRICT; -insert into arbiter_settings (id) values (1) on conflict do nothing; -- ensure singleton row exists +insert into arbiter_settings (id) values (1) on conflict do nothing; +-- ensure singleton row exists create table if not exists useragent_client ( id integer not null primary key, @@ -50,15 +51,37 @@ create table if not exists useragent_client ( created_at integer not null default(unixepoch ('now')), updated_at integer not null default(unixepoch ('now')) ) STRICT; +create unique index if not exists uniq_useragent_client_public_key on useragent_client (public_key, key_type); + +create table if not exists client_metadata ( + id integer not null primary key, + name text not null, -- human-readable name for the client + description text, -- optional description for the client + version text, -- client version for tracking and debugging + created_at integer not null default(unixepoch ('now')) +) STRICT; + +-- created to track history of changes +create table if not exists client_metadata_history ( + id integer not null primary key, + metadata_id integer not null references client_metadata (id) on delete cascade, + client_id integer not null references program_client (id) on delete cascade, + created_at integer not null default(unixepoch ('now')) +) STRICT; + +create unique index if not exists uniq_metadata_binding_client on client_metadata_history (client_id); create table if not exists program_client ( id integer not null primary key, nonce integer not null default(1), -- used for auth challenge public_key blob not null, + metadata_id integer not null references client_metadata (id) on delete cascade, created_at integer not null default(unixepoch ('now')), updated_at integer not null default(unixepoch ('now')) ) STRICT; +create unique index if not exists uniq_program_client_public_key on program_client (public_key); + create table if not exists evm_wallet ( id integer not null primary key, address blob not null, -- 20-byte Ethereum address @@ -67,93 +90,101 @@ create table if not exists evm_wallet ( ) STRICT; create unique index if not exists uniq_evm_wallet_address on evm_wallet (address); + create unique index if not exists uniq_evm_wallet_aead on evm_wallet (aead_encrypted_id); +create table if not exists evm_wallet_visibility ( + id integer not null primary key, + wallet_id integer not null references evm_wallet (id) on delete cascade, + client_id integer not null references program_client (id) on delete cascade, + created_at integer not null default(unixepoch ('now')) +) STRICT; + +create unique index if not exists uniq_wallet_visibility on evm_wallet_visibility (wallet_id, client_id); + create table if not exists evm_ether_transfer_limit ( id integer not null primary key, - window_secs integer not null, -- window duration in seconds - max_volume blob not null -- big-endian 32-byte U256 + window_secs integer not null, -- window duration in seconds + max_volume blob not null -- big-endian 32-byte U256 ) STRICT; -- Shared grant properties: client scope, timeframe, fee caps, and rate limit create table if not exists evm_basic_grant ( id integer not null primary key, - wallet_id integer not null references evm_wallet(id) on delete restrict, - client_id integer not null references program_client(id) on delete restrict, - chain_id integer not null, -- EIP-155 chain ID - valid_from integer, -- unix timestamp (seconds), null = no lower bound - valid_until integer, -- unix timestamp (seconds), null = no upper bound - max_gas_fee_per_gas blob, -- big-endian 32-byte U256, null = unlimited - max_priority_fee_per_gas blob, -- big-endian 32-byte U256, null = unlimited - rate_limit_count integer, -- max transactions in window, null = unlimited - rate_limit_window_secs integer, -- window duration in seconds, null = unlimited - revoked_at integer, -- unix timestamp when revoked, null = still active - created_at integer not null default(unixepoch('now')) + visibility_id integer not null references evm_wallet_visibility (id) on delete restrict, + chain_id integer not null, -- EIP-155 chain ID + valid_from integer, -- unix timestamp (seconds), null = no lower bound + valid_until integer, -- unix timestamp (seconds), null = no upper bound + max_gas_fee_per_gas blob, -- big-endian 32-byte U256, null = unlimited + max_priority_fee_per_gas blob, -- big-endian 32-byte U256, null = unlimited + rate_limit_count integer, -- max transactions in window, null = unlimited + rate_limit_window_secs integer, -- window duration in seconds, null = unlimited + revoked_at integer, -- unix timestamp when revoked, null = still active + created_at integer not null default(unixepoch ('now')) ) STRICT; -- Shared transaction log for all EVM grants, used for rate limit tracking and auditing create table if not exists evm_transaction_log ( id integer not null primary key, - grant_id integer not null references evm_basic_grant(id) on delete restrict, - client_id integer not null references program_client(id) on delete restrict, - wallet_id integer not null references evm_wallet(id) on delete restrict, + visibility_id integer not null references evm_wallet_visibility (id) on delete restrict, + grant_id integer not null references evm_basic_grant (id) on delete restrict, chain_id integer not null, - eth_value blob not null, -- always present on any EVM tx - signed_at integer not null default(unixepoch('now')) + eth_value blob not null, -- always present on any EVM tx + signed_at integer not null default(unixepoch ('now')) ) STRICT; -create index if not exists idx_evm_basic_grant_wallet_chain on evm_basic_grant(client_id, wallet_id, chain_id); +create index if not exists idx_evm_basic_grant_wallet_chain on evm_basic_grant (visibility_id, chain_id); -- =============================== -- ERC20 token transfer grant -- =============================== create table if not exists evm_token_transfer_grant ( id integer not null primary key, - basic_grant_id integer not null unique references evm_basic_grant(id) on delete cascade, - token_contract blob not null, -- 20-byte ERC20 contract address - receiver blob -- 20-byte recipient address or null if every recipient allowed + basic_grant_id integer not null unique references evm_basic_grant (id) on delete cascade, + token_contract blob not null, -- 20-byte ERC20 contract address + receiver blob -- 20-byte recipient address or null if every recipient allowed ) STRICT; -- Per-window volume limits for token transfer grants create table if not exists evm_token_transfer_volume_limit ( id integer not null primary key, - grant_id integer not null references evm_token_transfer_grant(id) on delete cascade, - window_secs integer not null, -- window duration in seconds - max_volume blob not null -- big-endian 32-byte U256 + grant_id integer not null references evm_token_transfer_grant (id) on delete cascade, + window_secs integer not null, -- window duration in seconds + max_volume blob not null -- big-endian 32-byte U256 ) STRICT; -- Log table for token transfer grant usage create table if not exists evm_token_transfer_log ( id integer not null primary key, - grant_id integer not null references evm_token_transfer_grant(id) on delete restrict, - log_id integer not null references evm_transaction_log(id) on delete restrict, - chain_id integer not null, -- EIP-155 chain ID - token_contract blob not null, -- 20-byte ERC20 contract address - recipient_address blob not null, -- 20-byte recipient address - value blob not null, -- big-endian 32-byte U256 - created_at integer not null default(unixepoch('now')) + grant_id integer not null references evm_token_transfer_grant (id) on delete restrict, + log_id integer not null references evm_transaction_log (id) on delete restrict, + chain_id integer not null, -- EIP-155 chain ID + token_contract blob not null, -- 20-byte ERC20 contract address + recipient_address blob not null, -- 20-byte recipient address + value blob not null, -- big-endian 32-byte U256 + created_at integer not null default(unixepoch ('now')) ) STRICT; -create index if not exists idx_token_transfer_log_grant on evm_token_transfer_log(grant_id); -create index if not exists idx_token_transfer_log_log_id on evm_token_transfer_log(log_id); -create index if not exists idx_token_transfer_log_chain on evm_token_transfer_log(chain_id); +create index if not exists idx_token_transfer_log_grant on evm_token_transfer_log (grant_id); +create index if not exists idx_token_transfer_log_log_id on evm_token_transfer_log (log_id); + +create index if not exists idx_token_transfer_log_chain on evm_token_transfer_log (chain_id); -- =============================== -- Ether transfer grant (uses base log) -- =============================== create table if not exists evm_ether_transfer_grant ( id integer not null primary key, - basic_grant_id integer not null unique references evm_basic_grant(id) on delete cascade, - limit_id integer not null references evm_ether_transfer_limit(id) on delete restrict + basic_grant_id integer not null unique references evm_basic_grant (id) on delete cascade, + limit_id integer not null references evm_ether_transfer_limit (id) on delete restrict ) STRICT; -- Specific recipient addresses for an ether transfer grant create table if not exists evm_ether_transfer_grant_target ( id integer not null primary key, - grant_id integer not null references evm_ether_transfer_grant(id) on delete cascade, - address blob not null -- 20-byte recipient address + grant_id integer not null references evm_ether_transfer_grant (id) on delete cascade, + address blob not null -- 20-byte recipient address ) STRICT; -create unique index if not exists uniq_ether_transfer_target on evm_ether_transfer_grant_target(grant_id, address); - +create unique index if not exists uniq_ether_transfer_target on evm_ether_transfer_grant_target (grant_id, address); diff --git a/server/crates/arbiter-server/src/actors/client/auth.rs b/server/crates/arbiter-server/src/actors/client/auth.rs index ffd425a..0b13ed9 100644 --- a/server/crates/arbiter-server/src/actors/client/auth.rs +++ b/server/crates/arbiter-server/src/actors/client/auth.rs @@ -2,8 +2,10 @@ use arbiter_proto::{ format_challenge, transport::{Bi, expect_message}, }; +use chrono::Utc; use diesel::{ - ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update, + ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, SelectableHelper as _, + dsl::insert_into, update, }; use diesel_async::RunQueryDsl as _; use ed25519_dalek::{Signature, VerifyingKey}; @@ -15,9 +17,20 @@ use crate::{ client::ClientConnection, router::{self, RequestClientApproval}, }, - db::{self, schema::program_client}, + db::{ + self, + models::{ProgramClientMetadata, SqliteTimestamp}, + schema::program_client, + }, }; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClientMetadata { + pub name: String, + pub description: Option, + pub version: Option, +} + #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] pub enum Error { #[error("Database pool unavailable")] @@ -44,8 +57,13 @@ pub enum ApproveError { #[derive(Debug, Clone)] pub enum Inbound { - AuthChallengeRequest { pubkey: VerifyingKey }, - AuthChallengeSolution { signature: Signature }, + AuthChallengeRequest { + pubkey: VerifyingKey, + metadata: ClientMetadata, + }, + AuthChallengeSolution { + signature: Signature, + }, } #[derive(Debug, Clone)] @@ -118,23 +136,37 @@ async fn approve_new_client( } } -async fn insert_client(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<(), Error> { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i32; +async fn insert_client( + db: &db::DatabasePool, + pubkey: &VerifyingKey, + metadata: &ClientMetadata, +) -> Result<(), Error> { + use crate::db::schema::client_metadata; let mut conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); Error::DatabasePoolUnavailable })?; + let metadata_id = insert_into(client_metadata::table) + .values(( + client_metadata::name.eq(&metadata.name), + client_metadata::description.eq(&metadata.description), + client_metadata::version.eq(&metadata.version), + )) + .returning(client_metadata::id) + .get_result::(&mut conn) + .await + .map_err(|e| { + error!(error = ?e, "Failed to insert client metadata"); + Error::DatabaseOperationFailed + })?; + insert_into(program_client::table) .values(( program_client::public_key.eq(pubkey.as_bytes().to_vec()), + program_client::metadata_id.eq(metadata_id), program_client::nonce.eq(1), // pre-incremented; challenge uses 0 - program_client::created_at.eq(now), - program_client::updated_at.eq(now), )) .execute(&mut conn) .await @@ -146,6 +178,95 @@ async fn insert_client(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<( Ok(()) } +async fn get_client_id(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result, Error> { + let mut conn = db.get().await.map_err(|e| { + error!(error = ?e, "Database pool error"); + Error::DatabasePoolUnavailable + })?; + + program_client::table + .filter(program_client::public_key.eq(pubkey.as_bytes().to_vec())) + .select(program_client::id) + .first::(&mut conn) + .await + .optional() + .map_err(|e| { + error!(error = ?e, "Database error"); + Error::DatabaseOperationFailed + }) +} + +async fn sync_client_metadata( + db: &db::DatabasePool, + client_id: i32, + metadata: &ClientMetadata, +) -> Result<(), Error> { + use crate::db::schema::{client_metadata, client_metadata_history}; + + let now = SqliteTimestamp(Utc::now()); + + let mut conn = db.get().await.map_err(|e| { + error!(error = ?e, "Database pool error"); + Error::DatabasePoolUnavailable + })?; + + conn.exclusive_transaction(|conn| { + let metadata = metadata.clone(); + Box::pin(async move { + let (current_metadata_id, current): (i32, ProgramClientMetadata) = + program_client::table + .find(client_id) + .inner_join(client_metadata::table) + .select(( + program_client::metadata_id, + ProgramClientMetadata::as_select(), + )) + .first(conn) + .await?; + + let unchanged = current.name == metadata.name + && current.description == metadata.description + && current.version == metadata.version; + if unchanged { + return Ok(()); + } + + insert_into(client_metadata_history::table) + .values(( + client_metadata_history::metadata_id.eq(current_metadata_id), + client_metadata_history::client_id.eq(client_id), + )) + .execute(conn) + .await?; + + let metadata_id = insert_into(client_metadata::table) + .values(( + client_metadata::name.eq(&metadata.name), + client_metadata::description.eq(&metadata.description), + client_metadata::version.eq(&metadata.version), + )) + .returning(client_metadata::id) + .get_result::(conn) + .await?; + + update(program_client::table.find(client_id)) + .set(( + program_client::metadata_id.eq(metadata_id), + program_client::updated_at.eq(now), + )) + .execute(conn) + .await?; + + Ok::<(), diesel::result::Error>(()) + }) + }) + .await + .map_err(|e| { + error!(error = ?e, "Database error"); + Error::DatabaseOperationFailed + }) +} + async fn challenge_client( transport: &mut T, pubkey: VerifyingKey, @@ -189,7 +310,7 @@ pub async fn authenticate( where T: Bi> + Send + ?Sized, { - let Some(Inbound::AuthChallengeRequest { pubkey }) = transport.recv().await else { + let Some(Inbound::AuthChallengeRequest { pubkey, metadata }) = transport.recv().await else { return Err(Error::Transport); }; @@ -197,11 +318,16 @@ where Some(nonce) => nonce, None => { approve_new_client(&props.actors, pubkey).await?; - insert_client(&props.db, &pubkey).await?; + insert_client(&props.db, &pubkey, &metadata).await?; 0 } }; + let client_id = get_client_id(&props.db, &pubkey) + .await? + .ok_or(Error::DatabaseOperationFailed)?; + sync_client_metadata(&props.db, client_id, &metadata).await?; + challenge_client(transport, pubkey, nonce).await?; transport .send(Ok(Outbound::AuthSuccess)) diff --git a/server/crates/arbiter-server/src/actors/evm/mod.rs b/server/crates/arbiter-server/src/actors/evm/mod.rs index 16e4200..39d63f7 100644 --- a/server/crates/arbiter-server/src/actors/evm/mod.rs +++ b/server/crates/arbiter-server/src/actors/evm/mod.rs @@ -148,31 +148,24 @@ impl EvmActor { #[message] pub async fn useragent_create_grant( &mut self, - client_id: i32, basic: SharedGrantSettings, grant: SpecificGrant, ) -> Result { match grant { SpecificGrant::EtherTransfer(settings) => { self.engine - .create_grant::( - client_id, - FullGrant { - basic, - specific: settings, - }, - ) + .create_grant::(FullGrant { + basic, + specific: settings, + }) .await } SpecificGrant::TokenTransfer(settings) => { self.engine - .create_grant::( - client_id, - FullGrant { - basic, - specific: settings, - }, - ) + .create_grant::(FullGrant { + basic, + specific: settings, + }) .await } } @@ -213,16 +206,19 @@ impl EvmActor { .await .optional()? .ok_or(SignTransactionError::WalletNotFound)?; + let visibility = schema::evm_wallet_visibility::table + .select(models::EvmWalletVisibility::as_select()) + .filter(schema::evm_wallet_visibility::wallet_id.eq(wallet.id)) + .filter(schema::evm_wallet_visibility::client_id.eq(client_id)) + .first(&mut conn) + .await + .optional()? + .ok_or(SignTransactionError::WalletNotFound)?; drop(conn); let meaning = self .engine - .evaluate_transaction( - wallet.id, - client_id, - transaction.clone(), - RunKind::Execution, - ) + .evaluate_transaction(visibility, transaction.clone(), RunKind::Execution) .await?; Ok(meaning) @@ -243,6 +239,14 @@ impl EvmActor { .await .optional()? .ok_or(SignTransactionError::WalletNotFound)?; + let visibility = schema::evm_wallet_visibility::table + .select(models::EvmWalletVisibility::as_select()) + .filter(schema::evm_wallet_visibility::wallet_id.eq(wallet.id)) + .filter(schema::evm_wallet_visibility::client_id.eq(client_id)) + .first(&mut conn) + .await + .optional()? + .ok_or(SignTransactionError::WalletNotFound)?; drop(conn); let raw_key: SafeCell> = self @@ -256,12 +260,7 @@ impl EvmActor { let signer = safe_signer::SafeSigner::from_cell(raw_key)?; self.engine - .evaluate_transaction( - wallet.id, - client_id, - transaction.clone(), - RunKind::Execution, - ) + .evaluate_transaction(visibility, transaction.clone(), RunKind::Execution) .await?; use alloy::network::TxSignerSync as _; diff --git a/server/crates/arbiter-server/src/actors/user_agent/mod.rs b/server/crates/arbiter-server/src/actors/user_agent/mod.rs index 7f454b7..986fbb5 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/mod.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/mod.rs @@ -1,4 +1,3 @@ - use crate::{ actors::GlobalActors, db::{self, models::KeyType}, diff --git a/server/crates/arbiter-server/src/actors/user_agent/session.rs b/server/crates/arbiter-server/src/actors/user_agent/session.rs index 6463479..d9a9337 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session.rs @@ -36,7 +36,10 @@ impl Error { pub struct UserAgentSession { props: UserAgentConnection, state: UserAgentStateMachine, - #[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>, } @@ -87,7 +90,7 @@ impl UserAgentSession { pub async fn request_new_client_approval( &mut self, client_pubkey: VerifyingKey, - cancel_flag: watch::Receiver<()>, + cancel_flag: watch::Receiver<()>, ) -> Result { // temporary use to make clippy happy while we refactor this flow dbg!(client_pubkey); diff --git a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs index 44b47c3..1156059 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session/connection.rs @@ -18,9 +18,9 @@ use crate::{ }, keyholder::{self, Bootstrap, TryUnseal}, user_agent::session::{ - UserAgentSession, - state::{UnsealContext, UserAgentEvents, UserAgentStates}, - }, + UserAgentSession, + state::{UnsealContext, UserAgentEvents, UserAgentStates}, + }, }, safe_cell::SafeCellHandle as _, }; @@ -312,7 +312,6 @@ impl UserAgentSession { #[message] pub(crate) async fn handle_grant_create( &mut self, - client_id: i32, basic: crate::evm::policies::SharedGrantSettings, grant: crate::evm::policies::SpecificGrant, ) -> Result { @@ -320,11 +319,7 @@ impl UserAgentSession { .props .actors .evm - .ask(UseragentCreateGrant { - client_id, - basic, - grant, - }) + .ask(UseragentCreateGrant { basic, grant }) .await { Ok(grant_id) => Ok(grant_id), diff --git a/server/crates/arbiter-server/src/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index ddf7773..f039364 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -21,7 +21,7 @@ pub mod types { sqlite::{Sqlite, SqliteType}, }; - #[derive(Debug, FromSqlRow, AsExpression)] + #[derive(Debug, FromSqlRow, AsExpression, Clone)] #[diesel(sql_type = Integer)] #[repr(transparent)] // hint compiler to optimize the wrapper struct away pub struct SqliteTimestamp(pub DateTime); @@ -185,12 +185,41 @@ pub struct EvmWallet { pub created_at: SqliteTimestamp, } -#[derive(Queryable, Debug, Insertable, Selectable)] +#[derive(Models, Queryable, Debug, Insertable, Selectable, Clone)] +#[diesel(table_name = schema::evm_wallet_visibility, check_for_backend(Sqlite))] +pub struct EvmWalletVisibility { + pub id: i32, + pub wallet_id: i32, + pub client_id: i32, + pub created_at: SqliteTimestamp, +} + +#[derive(Models, Queryable, Debug, Insertable, Selectable)] +#[diesel(table_name = schema::client_metadata, check_for_backend(Sqlite))] +pub struct ProgramClientMetadata { + pub id: i32, + pub name: String, + pub description: Option, + pub version: Option, + pub created_at: SqliteTimestamp, +} + +#[derive(Models, Queryable, Debug, Insertable, Selectable)] +#[diesel(table_name = schema::client_metadata_history, check_for_backend(Sqlite))] +pub struct ProgramClientMetadataHistory { + pub id: i32, + pub metadata_id: i32, + pub client_id: i32, + pub created_at: SqliteTimestamp, +} + +#[derive(Models, Queryable, Debug, Insertable, Selectable)] #[diesel(table_name = schema::program_client, check_for_backend(Sqlite))] pub struct ProgramClient { pub id: i32, pub nonce: i32, pub public_key: Vec, + pub metadata_id: i32, pub created_at: SqliteTimestamp, pub updated_at: SqliteTimestamp, } @@ -230,8 +259,7 @@ pub struct EvmEtherTransferLimit { )] pub struct EvmBasicGrant { pub id: i32, - pub wallet_id: i32, // references evm_wallet.id - pub client_id: i32, // references program_client.id + pub visibility_id: i32, // references evm_wallet_visibility.id pub chain_id: i32, pub valid_from: Option, pub valid_until: Option, @@ -254,8 +282,7 @@ pub struct EvmBasicGrant { pub struct EvmTransactionLog { pub id: i32, pub grant_id: i32, - pub client_id: i32, - pub wallet_id: i32, + pub visibility_id: i32, pub chain_id: i32, pub eth_value: Vec, pub signed_at: SqliteTimestamp, diff --git a/server/crates/arbiter-server/src/db/schema.rs b/server/crates/arbiter-server/src/db/schema.rs index 8d60f4b..28ec06e 100644 --- a/server/crates/arbiter-server/src/db/schema.rs +++ b/server/crates/arbiter-server/src/db/schema.rs @@ -20,11 +20,29 @@ diesel::table! { } } +diesel::table! { + client_metadata (id) { + id -> Integer, + name -> Text, + description -> Nullable, + version -> Nullable, + created_at -> Integer, + } +} + +diesel::table! { + client_metadata_history (id) { + id -> Integer, + metadata_id -> Integer, + client_id -> Integer, + created_at -> Integer, + } +} + diesel::table! { evm_basic_grant (id) { id -> Integer, - wallet_id -> Integer, - client_id -> Integer, + visibility_id -> Integer, chain_id -> Integer, valid_from -> Nullable, valid_until -> Nullable, @@ -95,9 +113,8 @@ diesel::table! { diesel::table! { evm_transaction_log (id) { id -> Integer, + visibility_id -> Integer, grant_id -> Integer, - client_id -> Integer, - wallet_id -> Integer, chain_id -> Integer, eth_value -> Binary, signed_at -> Integer, @@ -113,11 +130,21 @@ diesel::table! { } } +diesel::table! { + evm_wallet_visibility (id) { + id -> Integer, + wallet_id -> Integer, + client_id -> Integer, + created_at -> Integer, + } +} + diesel::table! { program_client (id) { id -> Integer, nonce -> Integer, public_key -> Binary, + metadata_id -> Integer, created_at -> Integer, updated_at -> Integer, } @@ -151,17 +178,18 @@ diesel::table! { id -> Integer, nonce -> Integer, public_key -> Binary, + key_type -> Integer, created_at -> Integer, updated_at -> Integer, - key_type -> Integer, } } diesel::joinable!(aead_encrypted -> root_key_history (associated_root_key_id)); diesel::joinable!(arbiter_settings -> root_key_history (root_key_id)); diesel::joinable!(arbiter_settings -> tls_history (tls_id)); -diesel::joinable!(evm_basic_grant -> evm_wallet (wallet_id)); -diesel::joinable!(evm_basic_grant -> program_client (client_id)); +diesel::joinable!(client_metadata_history -> client_metadata (metadata_id)); +diesel::joinable!(client_metadata_history -> program_client (client_id)); +diesel::joinable!(evm_basic_grant -> evm_wallet_visibility (visibility_id)); diesel::joinable!(evm_ether_transfer_grant -> evm_basic_grant (basic_grant_id)); diesel::joinable!(evm_ether_transfer_grant -> evm_ether_transfer_limit (limit_id)); diesel::joinable!(evm_ether_transfer_grant_target -> evm_ether_transfer_grant (grant_id)); @@ -169,11 +197,18 @@ diesel::joinable!(evm_token_transfer_grant -> evm_basic_grant (basic_grant_id)); diesel::joinable!(evm_token_transfer_log -> evm_token_transfer_grant (grant_id)); diesel::joinable!(evm_token_transfer_log -> evm_transaction_log (log_id)); diesel::joinable!(evm_token_transfer_volume_limit -> evm_token_transfer_grant (grant_id)); +diesel::joinable!(evm_transaction_log -> evm_basic_grant (grant_id)); +diesel::joinable!(evm_transaction_log -> evm_wallet_visibility (visibility_id)); diesel::joinable!(evm_wallet -> aead_encrypted (aead_encrypted_id)); +diesel::joinable!(evm_wallet_visibility -> evm_wallet (wallet_id)); +diesel::joinable!(evm_wallet_visibility -> program_client (client_id)); +diesel::joinable!(program_client -> client_metadata (metadata_id)); diesel::allow_tables_to_appear_in_same_query!( aead_encrypted, arbiter_settings, + client_metadata, + client_metadata_history, evm_basic_grant, evm_ether_transfer_grant, evm_ether_transfer_grant_target, @@ -183,6 +218,7 @@ diesel::allow_tables_to_appear_in_same_query!( evm_token_transfer_volume_limit, evm_transaction_log, evm_wallet, + evm_wallet_visibility, program_client, root_key_history, tls_history, diff --git a/server/crates/arbiter-server/src/evm/mod.rs b/server/crates/arbiter-server/src/evm/mod.rs index 503735b..4a2a7c5 100644 --- a/server/crates/arbiter-server/src/evm/mod.rs +++ b/server/crates/arbiter-server/src/evm/mod.rs @@ -6,13 +6,16 @@ use alloy::{ primitives::{TxKind, U256}, }; use chrono::Utc; -use diesel::{ExpressionMethods as _, QueryDsl, QueryResult, insert_into, sqlite::Sqlite}; +use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite}; use diesel_async::{AsyncConnection, RunQueryDsl}; use crate::{ db::{ self, - models::{EvmBasicGrant, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp}, + models::{ + EvmBasicGrant, EvmWalletVisibility, NewEvmBasicGrant, NewEvmTransactionLog, + SqliteTimestamp, + }, schema::{self, evm_transaction_log}, }, evm::policies::{ @@ -184,8 +187,7 @@ impl Engine { let log_id: i32 = insert_into(evm_transaction_log::table) .values(&NewEvmTransactionLog { grant_id: grant.shared_grant_id, - client_id: context.client_id, - wallet_id: context.wallet_id, + visibility_id: context.target.id, chain_id: context.chain as i32, eth_value: utils::u256_to_bytes(context.value).to_vec(), signed_at: Utc::now().into(), @@ -213,7 +215,6 @@ impl Engine { pub async fn create_grant( &self, - client_id: i32, full_grant: FullGrant, ) -> Result { let mut conn = self.db.get().await?; @@ -225,9 +226,8 @@ impl Engine { let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table) .values(&NewEvmBasicGrant { - wallet_id: full_grant.basic.wallet_id, chain_id: full_grant.basic.chain as i32, - client_id, + visibility_id: full_grant.basic.visibility_id, valid_from: full_grant.basic.valid_from.map(SqliteTimestamp), valid_until: full_grant.basic.valid_until.map(SqliteTimestamp), max_gas_fee_per_gas: full_grant @@ -295,8 +295,7 @@ impl Engine { pub async fn evaluate_transaction( &self, - wallet_id: i32, - client_id: i32, + target: EvmWalletVisibility, transaction: TxEip1559, run_kind: RunKind, ) -> Result { @@ -304,8 +303,7 @@ impl Engine { return Err(VetError::ContractCreationNotSupported); }; let context = policies::EvalContext { - wallet_id, - client_id, + target, chain: transaction.chain_id, to, value: transaction.value, diff --git a/server/crates/arbiter-server/src/evm/policies.rs b/server/crates/arbiter-server/src/evm/policies.rs index 32d3cd3..9e01361 100644 --- a/server/crates/arbiter-server/src/evm/policies.rs +++ b/server/crates/arbiter-server/src/evm/policies.rs @@ -10,7 +10,7 @@ use miette::Diagnostic; use thiserror::Error; use crate::{ - db::models::{self, EvmBasicGrant}, + db::models::{self, EvmBasicGrant, EvmWalletVisibility}, evm::utils, }; @@ -19,9 +19,8 @@ pub mod token_transfers; #[derive(Debug, Clone)] pub struct EvalContext { - // Which wallet is this transaction for - pub client_id: i32, - pub wallet_id: i32, + // Which wallet is this transaction for and who requested it + pub target: EvmWalletVisibility, // The transaction data pub chain: ChainId, @@ -145,8 +144,7 @@ pub struct VolumeRateLimit { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct SharedGrantSettings { - pub wallet_id: i32, - pub client_id: i32, + pub visibility_id: i32, pub chain: ChainId, pub valid_from: Option>, @@ -161,8 +159,7 @@ pub struct SharedGrantSettings { impl SharedGrantSettings { fn try_from_model(model: EvmBasicGrant) -> QueryResult { Ok(Self { - wallet_id: model.wallet_id, - client_id: model.client_id, + visibility_id: model.visibility_id, chain: model.chain_id as u64, // safe because chain_id is stored as i32 but is guaranteed to be a valid ChainId by the API when creating grants valid_from: model.valid_from.map(Into::into), valid_until: model.valid_until.map(Into::into), diff --git a/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs b/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs index e77d994..14e4931 100644 --- a/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs +++ b/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs @@ -196,9 +196,8 @@ impl Policy for EtherTransfer { .inner_join(evm_basic_grant::table) .inner_join(evm_ether_transfer_grant_target::table) .filter( - evm_basic_grant::wallet_id - .eq(context.wallet_id) - .and(evm_basic_grant::client_id.eq(context.client_id)) + evm_basic_grant::visibility_id + .eq(context.target.id) .and(evm_basic_grant::revoked_at.is_null()) .and(evm_ether_transfer_grant_target::address.eq(&target_bytes)), ) diff --git a/server/crates/arbiter-server/src/evm/policies/ether_transfer/tests.rs b/server/crates/arbiter-server/src/evm/policies/ether_transfer/tests.rs index 7e5dd9d..f8f1183 100644 --- a/server/crates/arbiter-server/src/evm/policies/ether_transfer/tests.rs +++ b/server/crates/arbiter-server/src/evm/policies/ether_transfer/tests.rs @@ -5,7 +5,9 @@ use diesel_async::RunQueryDsl; use crate::db::{ self, DatabaseConnection, - models::{EvmBasicGrant, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp}, + models::{ + EvmBasicGrant, EvmWalletVisibility, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp, + }, schema::{evm_basic_grant, evm_transaction_log}, }; use crate::evm::{ @@ -15,8 +17,7 @@ use crate::evm::{ use super::{EtherTransfer, Settings}; -const WALLET_ID: i32 = 1; -const CLIENT_ID: i32 = 2; +const VISIBILITY_ID: i32 = 1; const CHAIN_ID: u64 = 1; const ALLOWED: Address = address!("1111111111111111111111111111111111111111"); @@ -24,8 +25,12 @@ const OTHER: Address = address!("2222222222222222222222222222222222222222"); fn ctx(to: Address, value: U256) -> EvalContext { EvalContext { - wallet_id: WALLET_ID, - client_id: CLIENT_ID, + target: EvmWalletVisibility { + id: VISIBILITY_ID, + wallet_id: 10, + client_id: 20, + created_at: SqliteTimestamp(Utc::now()), + }, chain: CHAIN_ID, to, value, @@ -38,8 +43,7 @@ fn ctx(to: Address, value: U256) -> EvalContext { async fn insert_basic(conn: &mut DatabaseConnection, revoked: bool) -> EvmBasicGrant { insert_into(evm_basic_grant::table) .values(NewEvmBasicGrant { - wallet_id: WALLET_ID, - client_id: CLIENT_ID, + visibility_id: VISIBILITY_ID, chain_id: CHAIN_ID as i32, valid_from: None, valid_until: None, @@ -67,14 +71,13 @@ fn make_settings(targets: Vec
, max_volume: u64) -> Settings { fn shared() -> SharedGrantSettings { SharedGrantSettings { - wallet_id: WALLET_ID, + visibility_id: VISIBILITY_ID, chain: CHAIN_ID, valid_from: None, valid_until: None, max_gas_fee_per_gas: None, max_priority_fee_per_gas: None, rate_limit: None, - client_id: CLIENT_ID, } } @@ -153,8 +156,7 @@ async fn evaluate_passes_when_volume_within_limit() { insert_into(evm_transaction_log::table) .values(NewEvmTransactionLog { grant_id, - client_id: CLIENT_ID, - wallet_id: WALLET_ID, + visibility_id: VISIBILITY_ID, chain_id: CHAIN_ID as i32, eth_value: utils::u256_to_bytes(U256::from(500u64)).to_vec(), signed_at: SqliteTimestamp(Utc::now()), @@ -194,8 +196,7 @@ async fn evaluate_rejects_volume_over_limit() { insert_into(evm_transaction_log::table) .values(NewEvmTransactionLog { grant_id, - client_id: CLIENT_ID, - wallet_id: WALLET_ID, + visibility_id: VISIBILITY_ID, chain_id: CHAIN_ID as i32, eth_value: utils::u256_to_bytes(U256::from(1_001u64)).to_vec(), signed_at: SqliteTimestamp(Utc::now()), @@ -236,8 +237,7 @@ async fn evaluate_passes_at_exactly_volume_limit() { insert_into(evm_transaction_log::table) .values(NewEvmTransactionLog { grant_id, - client_id: CLIENT_ID, - wallet_id: WALLET_ID, + visibility_id: VISIBILITY_ID, chain_id: CHAIN_ID as i32, eth_value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(), signed_at: SqliteTimestamp(Utc::now()), diff --git a/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs b/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs index 34378ed..6791e24 100644 --- a/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs +++ b/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs @@ -209,8 +209,7 @@ impl Policy for TokenTransfer { let grant: Option<(EvmBasicGrant, EvmTokenTransferGrant)> = grant_join() .filter(evm_basic_grant::revoked_at.is_null()) - .filter(evm_basic_grant::wallet_id.eq(context.wallet_id)) - .filter(evm_basic_grant::client_id.eq(context.client_id)) + .filter(evm_basic_grant::visibility_id.eq(context.target.id)) .filter(evm_token_transfer_grant::token_contract.eq(&token_contract_bytes)) .select(( EvmBasicGrant::as_select(), diff --git a/server/crates/arbiter-server/src/evm/policies/token_transfers/tests.rs b/server/crates/arbiter-server/src/evm/policies/token_transfers/tests.rs index e41772a..f927284 100644 --- a/server/crates/arbiter-server/src/evm/policies/token_transfers/tests.rs +++ b/server/crates/arbiter-server/src/evm/policies/token_transfers/tests.rs @@ -6,7 +6,7 @@ use diesel_async::RunQueryDsl; use crate::db::{ self, DatabaseConnection, - models::{EvmBasicGrant, NewEvmBasicGrant, SqliteTimestamp}, + models::{EvmBasicGrant, EvmWalletVisibility, NewEvmBasicGrant, SqliteTimestamp}, schema::evm_basic_grant, }; use crate::evm::{ @@ -21,8 +21,7 @@ use super::{Settings, TokenTransfer}; const CHAIN_ID: u64 = 1; const DAI: Address = address!("6B175474E89094C44Da98b954EedeAC495271d0F"); -const WALLET_ID: i32 = 1; -const CLIENT_ID: i32 = 2; +const VISIBILITY_ID: i32 = 1; const RECIPIENT: Address = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); const OTHER: Address = address!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); @@ -38,8 +37,12 @@ fn transfer_calldata(to: Address, value: U256) -> Bytes { fn ctx(to: Address, calldata: Bytes) -> EvalContext { EvalContext { - wallet_id: WALLET_ID, - client_id: CLIENT_ID, + target: EvmWalletVisibility { + id: VISIBILITY_ID, + wallet_id: 10, + client_id: 20, + created_at: SqliteTimestamp(Utc::now()), + }, chain: CHAIN_ID, to, value: U256::ZERO, @@ -52,8 +55,7 @@ fn ctx(to: Address, calldata: Bytes) -> EvalContext { async fn insert_basic(conn: &mut DatabaseConnection, revoked: bool) -> EvmBasicGrant { insert_into(evm_basic_grant::table) .values(NewEvmBasicGrant { - wallet_id: WALLET_ID, - client_id: CLIENT_ID, + visibility_id: VISIBILITY_ID, chain_id: CHAIN_ID as i32, valid_from: None, valid_until: None, @@ -86,14 +88,13 @@ fn make_settings(target: Option
, max_volume: Option) -> Settings { fn shared() -> SharedGrantSettings { SharedGrantSettings { - wallet_id: WALLET_ID, + visibility_id: VISIBILITY_ID, chain: CHAIN_ID, valid_from: None, valid_until: None, max_gas_fee_per_gas: None, max_priority_fee_per_gas: None, rate_limit: None, - client_id: CLIENT_ID, } } diff --git a/server/crates/arbiter-server/src/grpc/client.rs b/server/crates/arbiter-server/src/grpc/client.rs index 2fb1d24..063a5b2 100644 --- a/server/crates/arbiter-server/src/grpc/client.rs +++ b/server/crates/arbiter-server/src/grpc/client.rs @@ -110,9 +110,8 @@ async fn dispatch_conn_message( pub async fn start(conn: ClientConnection, mut bi: GrpcBi) { let mut conn = conn; let mut request_tracker = RequestTracker::default(); - let mut response_id = None; - match auth::start(&mut conn, &mut bi, &mut request_tracker, &mut response_id).await { + match auth::start(&mut conn, &mut bi, &mut request_tracker).await { Ok(_) => { let actor = client::session::ClientSession::spawn(client::session::ClientSession::new(conn)); @@ -125,11 +124,7 @@ pub async fn start(conn: ClientConnection, mut bi: GrpcBi { - let mut transport = auth::AuthTransportAdapter::new( - &mut bi, - &mut request_tracker, - &mut response_id, - ); + let mut transport = auth::AuthTransportAdapter::new(&mut bi, &mut request_tracker); let _ = transport.send(Err(e.clone())).await; warn!(error = ?e, "Authentication failed"); } diff --git a/server/crates/arbiter-server/src/grpc/client/auth.rs b/server/crates/arbiter-server/src/grpc/client/auth.rs index 49d8d55..8177810 100644 --- a/server/crates/arbiter-server/src/grpc/client/auth.rs +++ b/server/crates/arbiter-server/src/grpc/client/auth.rs @@ -2,7 +2,8 @@ use arbiter_proto::{ proto::client::{ AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest, AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult, - ClientRequest, ClientResponse, client_request::Payload as ClientRequestPayload, + ClientInfo as ProtoClientInfo, ClientRequest, ClientResponse, + client_request::Payload as ClientRequestPayload, client_response::Payload as ClientResponsePayload, }, transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}, @@ -19,19 +20,16 @@ use crate::{ pub struct AuthTransportAdapter<'a> { bi: &'a mut GrpcBi, request_tracker: &'a mut RequestTracker, - response_id: &'a mut Option, } impl<'a> AuthTransportAdapter<'a> { pub fn new( bi: &'a mut GrpcBi, request_tracker: &'a mut RequestTracker, - response_id: &'a mut Option, ) -> Self { Self { bi, request_tracker, - response_id, } } @@ -72,11 +70,9 @@ impl<'a> AuthTransportAdapter<'a> { &mut self, payload: ClientResponsePayload, ) -> Result<(), TransportError> { - let request_id = self.response_id.take(); - self.bi .send(Ok(ClientResponse { - request_id, + request_id: Some(self.request_tracker.current_request_id()), payload: Some(payload), })) .await @@ -114,19 +110,27 @@ impl Receiver for AuthTransportAdapter<'_> { } }; - let request_id = match self.request_tracker.request(request.request_id) { + match self.request_tracker.request(request.request_id) { Ok(request_id) => request_id, Err(error) => { let _ = self.bi.send(Err(error)).await; return None; } }; - *self.response_id = Some(request_id); - let payload = request.payload?; match payload { - ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest { pubkey }) => { + ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest { + pubkey, + client_info, + }) => { + let Some(client_info) = client_info else { + let _ = self + .bi + .send(Err(Status::invalid_argument("Missing client info"))) + .await; + return None; + }; let Ok(pubkey) = <[u8; 32]>::try_from(pubkey) else { let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await; return None; @@ -135,7 +139,10 @@ impl Receiver for AuthTransportAdapter<'_> { let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await; return None; }; - Some(auth::Inbound::AuthChallengeRequest { pubkey }) + Some(auth::Inbound::AuthChallengeRequest { + pubkey, + metadata: client_metadata_from_proto(client_info), + }) } ClientRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution { signature, @@ -151,7 +158,9 @@ impl Receiver 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 } @@ -161,13 +170,20 @@ impl Receiver for AuthTransportAdapter<'_> { impl Bi> for AuthTransportAdapter<'_> {} +fn client_metadata_from_proto(metadata: ProtoClientInfo) -> auth::ClientMetadata { + auth::ClientMetadata { + name: metadata.name, + description: (!metadata.description.is_empty()).then_some(metadata.description), + version: (!metadata.version.is_empty()).then_some(metadata.version), + } +} + pub async fn start( conn: &mut ClientConnection, bi: &mut GrpcBi, request_tracker: &mut RequestTracker, - response_id: &mut Option, ) -> Result<(), auth::Error> { - let mut transport = AuthTransportAdapter::new(bi, request_tracker, response_id); + let mut transport = AuthTransportAdapter::new(bi, request_tracker); client::auth::authenticate(conn, &mut transport).await?; Ok(()) } diff --git a/server/crates/arbiter-server/src/grpc/request_tracker.rs b/server/crates/arbiter-server/src/grpc/request_tracker.rs index e282343..7ab6254 100644 --- a/server/crates/arbiter-server/src/grpc/request_tracker.rs +++ b/server/crates/arbiter-server/src/grpc/request_tracker.rs @@ -17,4 +17,10 @@ impl RequestTracker { Ok(id) } + + // This is used to set the response id for auth responses, which need to match the request id of the auth challenge request. + // -1 offset is needed because request() increments the next_request_id after returning the current request id. + pub fn current_request_id(&self) -> i32 { + self.next_request_id - 1 + } } diff --git a/server/crates/arbiter-server/src/grpc/user_agent.rs b/server/crates/arbiter-server/src/grpc/user_agent.rs index c3fb347..96aef5f 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent.rs @@ -241,11 +241,7 @@ async fn dispatch_conn_message( UserAgentRequestPayload::EvmGrantList(_) => UserAgentResponsePayload::EvmGrantList( EvmGrantOrWallet::grant_list_response(actor.ask(HandleGrantList {}).await), ), - UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest { - client_id, - shared, - specific, - }) => { + UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest { shared, specific }) => { let (basic, grant) = match parse_grant_request(shared, specific) { Ok(values) => values, Err(status) => { @@ -255,13 +251,7 @@ async fn dispatch_conn_message( }; UserAgentResponsePayload::EvmGrantCreate(EvmGrantOrWallet::grant_create_response( - actor - .ask(HandleGrantCreate { - client_id, - basic, - grant, - }) - .await, + actor.ask(HandleGrantCreate { basic, grant }).await, )) } UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id }) => { @@ -296,6 +286,7 @@ async fn send_out_of_band( OutOfBand::ClientConnectionRequest { pubkey } => { UserAgentResponsePayload::ClientConnectionRequest(ClientConnectionRequest { pubkey: pubkey.to_bytes().to_vec(), + info: None, }) } OutOfBand::ClientConnectionCancel => { @@ -327,8 +318,7 @@ fn parse_grant_request( fn shared_settings_from_proto(shared: ProtoSharedSettings) -> Result { Ok(SharedGrantSettings { - wallet_id: shared.wallet_id, - client_id: 0, + visibility_id: shared.visibility_id, chain: shared.chain_id, valid_from: shared.valid_from.map(proto_timestamp_to_utc).transpose()?, valid_until: shared.valid_until.map(proto_timestamp_to_utc).transpose()?, @@ -412,7 +402,7 @@ fn proto_timestamp_to_utc( fn shared_settings_to_proto(shared: SharedGrantSettings) -> ProtoSharedSettings { ProtoSharedSettings { - wallet_id: shared.wallet_id, + visibility_id: shared.visibility_id, chain_id: shared.chain, valid_from: shared.valid_from.map(|time| prost_types::Timestamp { seconds: time.timestamp(), @@ -552,7 +542,7 @@ impl EvmGrantOrWallet { .into_iter() .map(|grant| GrantEntry { id: grant.id, - client_id: grant.shared.client_id, + visibility_id: grant.shared.visibility_id, shared: Some(shared_settings_to_proto(grant.shared)), specific: Some(specific_grant_to_proto(grant.settings)), }) @@ -575,15 +565,8 @@ pub async fn start( mut bi: GrpcBi, ) { let mut request_tracker = RequestTracker::default(); - let mut response_id = None; - let pubkey = match auth::start( - &mut conn, - &mut bi, - &mut request_tracker, - &mut response_id, - ) - .await + let pubkey = match auth::start(&mut conn, &mut bi, &mut request_tracker).await { Ok(pubkey) => pubkey, Err(e) => { diff --git a/server/crates/arbiter-server/src/grpc/user_agent/auth.rs b/server/crates/arbiter-server/src/grpc/user_agent/auth.rs index 024190d..578b849 100644 --- a/server/crates/arbiter-server/src/grpc/user_agent/auth.rs +++ b/server/crates/arbiter-server/src/grpc/user_agent/auth.rs @@ -21,19 +21,16 @@ use crate::{ pub struct AuthTransportAdapter<'a> { bi: &'a mut GrpcBi, request_tracker: &'a mut RequestTracker, - response_id: &'a mut Option, } impl<'a> AuthTransportAdapter<'a> { pub fn new( bi: &'a mut GrpcBi, request_tracker: &'a mut RequestTracker, - response_id: &'a mut Option, ) -> Self { Self { bi, request_tracker, - response_id, } } @@ -41,11 +38,9 @@ impl<'a> AuthTransportAdapter<'a> { &mut self, payload: UserAgentResponsePayload, ) -> Result<(), TransportError> { - let id = self.response_id.take(); - self.bi .send(Ok(UserAgentResponse { - id, + id: Some(self.request_tracker.current_request_id()), payload: Some(payload), })) .await @@ -75,9 +70,14 @@ impl Sender> for AuthTransportAdapter<'_> { Err(Error::InvalidBootstrapToken) => { UserAgentResponsePayload::AuthResult(ProtoAuthResult::TokenInvalid.into()) } - Err(Error::Internal { details }) => return self.bi.send(Err(Status::internal(details))).await, + Err(Error::Internal { details }) => { + return self.bi.send(Err(Status::internal(details))).await; + } Err(Error::Transport) => { - return self.bi.send(Err(Status::unavailable("transport error"))).await; + return self + .bi + .send(Err(Status::unavailable("transport error"))) + .await; } }; @@ -96,14 +96,13 @@ impl Receiver for AuthTransportAdapter<'_> { } }; - let request_id = match self.request_tracker.request(request.id) { + match self.request_tracker.request(request.id) { Ok(request_id) => request_id, Err(error) => { let _ = self.bi.send(Err(error)).await; return None; } }; - *self.response_id = Some(request_id); let Some(payload) = request.payload else { warn!( @@ -173,8 +172,7 @@ pub async fn start( conn: &mut UserAgentConnection, bi: &mut GrpcBi, request_tracker: &mut RequestTracker, - response_id: &mut Option, ) -> Result { - let transport = AuthTransportAdapter::new(bi, request_tracker, response_id); + let transport = AuthTransportAdapter::new(bi, request_tracker); auth::authenticate(conn, transport).await } diff --git a/server/crates/arbiter-server/src/lib.rs b/server/crates/arbiter-server/src/lib.rs index 412def2..13941e4 100644 --- a/server/crates/arbiter-server/src/lib.rs +++ b/server/crates/arbiter-server/src/lib.rs @@ -11,9 +11,6 @@ pub mod grpc; pub mod safe_cell; pub mod utils; -#[allow(dead_code, reason = "Reserved as the shared default channel size while server wiring is still being consolidated")] -const DEFAULT_CHANNEL_SIZE: usize = 1000; - pub struct Server { context: ServerContext, } diff --git a/server/crates/arbiter-server/tests/client/auth.rs b/server/crates/arbiter-server/tests/client/auth.rs index ca1d0d0..910d9d3 100644 --- a/server/crates/arbiter-server/tests/client/auth.rs +++ b/server/crates/arbiter-server/tests/client/auth.rs @@ -2,14 +2,50 @@ use arbiter_proto::transport::{Receiver, Sender}; use arbiter_server::actors::GlobalActors; use arbiter_server::{ actors::client::{ClientConnection, auth, connect_client}, - db::{self, schema}, + db, }; -use diesel::{ExpressionMethods as _, insert_into}; +use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into}; use diesel_async::RunQueryDsl; use ed25519_dalek::Signer as _; use super::common::ChannelTransport; +fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> auth::ClientMetadata { + auth::ClientMetadata { + name: name.to_owned(), + description: description.map(str::to_owned), + version: version.map(str::to_owned), + } +} + +async fn insert_registered_client( + db: &db::DatabasePool, + pubkey: Vec, + metadata: &auth::ClientMetadata, +) { + use arbiter_server::db::schema::{client_metadata, program_client}; + + let mut conn = db.get().await.unwrap(); + let metadata_id: i32 = insert_into(client_metadata::table) + .values(( + client_metadata::name.eq(&metadata.name), + client_metadata::description.eq(&metadata.description), + client_metadata::version.eq(&metadata.version), + )) + .returning(client_metadata::id) + .get_result(&mut conn) + .await + .unwrap(); + insert_into(program_client::table) + .values(( + program_client::public_key.eq(pubkey), + program_client::metadata_id.eq(metadata_id), + )) + .execute(&mut conn) + .await + .unwrap(); +} + #[tokio::test] #[test_log::test] pub async fn test_unregistered_pubkey_rejected() { @@ -28,6 +64,7 @@ pub async fn test_unregistered_pubkey_rejected() { test_transport .send(auth::Inbound::AuthChallengeRequest { pubkey: new_key.verifying_key(), + metadata: metadata("client", Some("desc"), Some("1.0.0")), }) .await .unwrap(); @@ -44,14 +81,12 @@ pub async fn test_challenge_auth() { let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); - { - let mut conn = db.get().await.unwrap(); - insert_into(schema::program_client::table) - .values(schema::program_client::public_key.eq(pubkey_bytes.clone())) - .execute(&mut conn) - .await - .unwrap(); - } + insert_registered_client( + &db, + pubkey_bytes.clone(), + &metadata("client", Some("desc"), Some("1.0.0")), + ) + .await; let (server_transport, mut test_transport) = ChannelTransport::new(); let actors = GlobalActors::spawn(db.clone()).await.unwrap(); @@ -66,6 +101,7 @@ pub async fn test_challenge_auth() { test_transport .send(auth::Inbound::AuthChallengeRequest { pubkey: new_key.verifying_key(), + metadata: metadata("client", Some("desc"), Some("1.0.0")), }) .await .unwrap(); @@ -105,3 +141,187 @@ pub async fn test_challenge_auth() { // Auth completes, session spawned task.await.unwrap(); } + +#[tokio::test] +#[test_log::test] +pub async fn test_metadata_unchanged_does_not_append_history() { + let db = db::create_test_pool().await; + let actors = GlobalActors::spawn(db.clone()).await.unwrap(); + let props = ClientConnection::new(db.clone(), actors); + + let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let requested = metadata("client", Some("desc"), Some("1.0.0")); + + { + use arbiter_server::db::schema::{ + client_metadata, program_client, + }; + let mut conn = db.get().await.unwrap(); + let metadata_id: i32 = insert_into(client_metadata::table) + .values(( + client_metadata::name.eq(&requested.name), + client_metadata::description.eq(&requested.description), + client_metadata::version.eq(&requested.version), + )) + .returning(client_metadata::id) + .get_result(&mut conn) + .await + .unwrap(); + insert_into(program_client::table) + .values(( + program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()), + program_client::metadata_id.eq(metadata_id), + )) + .execute(&mut conn) + .await + .unwrap(); + } + + let (server_transport, mut test_transport) = ChannelTransport::new(); + let task = tokio::spawn(async move { + let mut server_transport = server_transport; + connect_client(props, &mut server_transport).await; + }); + + test_transport + .send(auth::Inbound::AuthChallengeRequest { + pubkey: new_key.verifying_key(), + metadata: requested, + }) + .await + .unwrap(); + + let response = test_transport.recv().await.unwrap().unwrap(); + let (pubkey, nonce) = match response { + auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce), + other => panic!("Expected AuthChallenge, got {other:?}"), + }; + let signature = new_key.sign(&arbiter_proto::format_challenge(nonce, pubkey.as_bytes())); + test_transport + .send(auth::Inbound::AuthChallengeSolution { signature }) + .await + .unwrap(); + let _ = test_transport.recv().await.unwrap(); + task.await.unwrap(); + + { + use arbiter_server::db::schema::{client_metadata, client_metadata_history}; + let mut conn = db.get().await.unwrap(); + let metadata_count: i64 = client_metadata::table + .count() + .get_result(&mut conn) + .await + .unwrap(); + let history_count: i64 = client_metadata_history::table + .count() + .get_result(&mut conn) + .await + .unwrap(); + assert_eq!(metadata_count, 1); + assert_eq!(history_count, 0); + } +} + +#[tokio::test] +#[test_log::test] +pub async fn test_metadata_change_appends_history_and_repoints_binding() { + let db = db::create_test_pool().await; + let actors = GlobalActors::spawn(db.clone()).await.unwrap(); + let props = ClientConnection::new(db.clone(), actors); + + let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + + { + use arbiter_server::db::schema::{ + client_metadata, program_client, + }; + let mut conn = db.get().await.unwrap(); + let metadata_id: i32 = insert_into(client_metadata::table) + .values(( + client_metadata::name.eq("client"), + client_metadata::description.eq(Some("old")), + client_metadata::version.eq(Some("1.0.0")), + )) + .returning(client_metadata::id) + .get_result(&mut conn) + .await + .unwrap(); + insert_into(program_client::table) + .values(( + program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()), + program_client::metadata_id.eq(metadata_id), + )) + .execute(&mut conn) + .await + .unwrap(); + } + + let (server_transport, mut test_transport) = ChannelTransport::new(); + let task = tokio::spawn(async move { + let mut server_transport = server_transport; + connect_client(props, &mut server_transport).await; + }); + + test_transport + .send(auth::Inbound::AuthChallengeRequest { + pubkey: new_key.verifying_key(), + metadata: metadata("client", Some("new"), Some("2.0.0")), + }) + .await + .unwrap(); + + let response = test_transport.recv().await.unwrap().unwrap(); + let (pubkey, nonce) = match response { + auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce), + other => panic!("Expected AuthChallenge, got {other:?}"), + }; + let signature = new_key.sign(&arbiter_proto::format_challenge(nonce, pubkey.as_bytes())); + test_transport + .send(auth::Inbound::AuthChallengeSolution { signature }) + .await + .unwrap(); + let _ = test_transport.recv().await.unwrap(); + task.await.unwrap(); + + { + use arbiter_server::db::schema::{ + client_metadata, client_metadata_history, program_client, + }; + let mut conn = db.get().await.unwrap(); + let metadata_count: i64 = client_metadata::table + .count() + .get_result(&mut conn) + .await + .unwrap(); + let history_count: i64 = client_metadata_history::table + .count() + .get_result(&mut conn) + .await + .unwrap(); + let metadata_id = program_client::table + .select(program_client::metadata_id) + .first::(&mut conn) + .await + .unwrap(); + let current = client_metadata::table + .find(metadata_id) + .select(( + client_metadata::name, + client_metadata::description.nullable(), + client_metadata::version.nullable(), + )) + .first::<(String, Option, Option)>(&mut conn) + .await + .unwrap(); + assert_eq!(metadata_count, 2); + assert_eq!(history_count, 1); + assert_eq!( + current, + ( + "client".to_owned(), + Some("new".to_owned()), + Some("2.0.0".to_owned()) + ) + ); + } +} diff --git a/server/crates/arbiter-tokens-registry/src/evm/mod.rs b/server/crates/arbiter-tokens-registry/src/evm/mod.rs index 34f9e88..1cf332b 100644 --- a/server/crates/arbiter-tokens-registry/src/evm/mod.rs +++ b/server/crates/arbiter-tokens-registry/src/evm/mod.rs @@ -1 +1 @@ -pub mod nonfungible; \ No newline at end of file +pub mod nonfungible; diff --git a/server/crates/arbiter-tokens-registry/src/lib.rs b/server/crates/arbiter-tokens-registry/src/lib.rs index bc0765f..c469d0c 100644 --- a/server/crates/arbiter-tokens-registry/src/lib.rs +++ b/server/crates/arbiter-tokens-registry/src/lib.rs @@ -1 +1 @@ -pub mod evm; \ No newline at end of file +pub mod evm;