diff --git a/mise.toml b/mise.toml index f0da5e6..a1f3729 100644 --- a/mise.toml +++ b/mise.toml @@ -22,3 +22,5 @@ run = ''' dart pub global activate protoc_plugin && \ protoc --dart_out=grpc:useragent/lib/proto --proto_path=protobufs/ $(find protobufs -name '*.proto' | sort) ''' + +[tasks.generate_schema] 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 0254158..7eab3f7 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 @@ -57,7 +57,6 @@ create table if not exists operator ( share blob not null, share_nonce blob not null, - created_at integer not null default(unixepoch ('now')), updated_at integer not null default(unixepoch ('now')) diff --git a/server/crates/arbiter-server/src/actors/evm/mod.rs b/server/crates/arbiter-server/src/actors/evm/mod.rs index 11392ec..033d11f 100644 --- a/server/crates/arbiter-server/src/actors/evm/mod.rs +++ b/server/crates/arbiter-server/src/actors/evm/mod.rs @@ -3,7 +3,7 @@ use crate::{ crypto::integrity, db::{ DatabaseError, DatabasePool, - models::{self}, + models::{self, EvmWalletId}, schema, }, evm::{ @@ -116,7 +116,7 @@ impl EvmActor { } #[message] - pub async fn list_wallets(&self) -> Result, Error> { + pub async fn list_wallets(&self) -> Result, Error> { let mut conn = self.db.get().await.map_err(DatabaseError::from)?; let rows: Vec = schema::evm_wallet::table .select(models::EvmWallet::as_select()) diff --git a/server/crates/arbiter-server/src/actors/vault/mod.rs b/server/crates/arbiter-server/src/actors/vault/mod.rs index e290dcf..62aae8e 100644 --- a/server/crates/arbiter-server/src/actors/vault/mod.rs +++ b/server/crates/arbiter-server/src/actors/vault/mod.rs @@ -6,7 +6,7 @@ use crate::{ }, db::{ self, - models::{self, RootKeyHistory}, + models::{self, RootKeyHistory, RootKeyHistoryId}, schema::{self}, }, }; @@ -25,7 +25,6 @@ use strum::{EnumDiscriminants, IntoDiscriminant}; use tracing::{error, info}; pub mod events { - #[derive(Clone, Copy)] pub struct Bootstrapped; @@ -64,7 +63,7 @@ pub enum Error { } struct Unsealed { - root_key_history_id: i32, + root_key_history_id: RootKeyHistoryId, root_key: KeyCell, } @@ -73,8 +72,9 @@ struct Unsealed { enum State { #[default] Unbootstrapped, + Sealed { - root_key_history_id: i32, + root_key_history_id: RootKeyHistoryId, }, Unsealed(Unsealed), } @@ -115,7 +115,10 @@ impl Vault { // Exclusive transaction to avoid race condtions if multiple vaults write // additional layer of protection against nonce-reuse - async fn get_new_nonce(pool: &db::DatabasePool, root_key_id: i32) -> Result { + async fn get_new_nonce( + pool: &db::DatabasePool, + root_key_id: RootKeyHistoryId, + ) -> Result { let mut conn = pool.get().await?; let nonce = conn @@ -129,7 +132,7 @@ impl Vault { let mut nonce = Nonce::try_from(current_nonce.as_slice()).map_err(|()| { error!( - "Broken database: invalid nonce for root key history id={}", + "Broken database: invalid nonce for root key history id={:#?}", root_key_id ); Error::BrokenDatabase @@ -187,18 +190,19 @@ impl Vault { let root_key_history_id = conn .transaction(|conn| { Box::pin(async move { - let root_key_history_id: i32 = insert_into(schema::root_key_history::table) - .values(&models::NewRootKeyHistory { - ciphertext: root_key_ciphertext, - tag: v1::ROOT_KEY_TAG.to_vec(), - root_key_encryption_nonce: root_key_nonce.to_vec(), - data_encryption_nonce: data_encryption_nonce_bytes, - schema_version: 1, - salt: salt.to_vec(), - }) - .returning(schema::root_key_history::id) - .get_result(conn) - .await?; + let root_key_history_id: RootKeyHistoryId = + insert_into(schema::root_key_history::table) + .values(&models::NewRootKeyHistory { + ciphertext: root_key_ciphertext, + tag: v1::ROOT_KEY_TAG.to_vec(), + root_key_encryption_nonce: root_key_nonce.to_vec(), + data_encryption_nonce: data_encryption_nonce_bytes, + schema_version: 1, + salt: salt.to_vec(), + }) + .returning(schema::root_key_history::id) + .get_result(conn) + .await?; update(schema::arbiter_settings::table) .set(schema::arbiter_settings::root_key_id.eq(root_key_history_id)) @@ -344,7 +348,10 @@ impl Vault { } #[message] - pub fn sign_integrity(&mut self, mac_input: Vec) -> Result<(i32, Vec), Error> { + pub fn sign_integrity( + &mut self, + mac_input: Vec, + ) -> Result<(RootKeyHistoryId, Vec), Error> { let Unsealed { root_key, root_key_history_id, @@ -356,7 +363,7 @@ impl Vault { Ok(v) => v, Err(_) => unreachable!("HMAC accepts keys of any size"), }); - hmac.update(&root_key_history_id.to_be_bytes()); + hmac.update(&root_key_history_id.to_raw().to_be_bytes()); hmac.update(&mac_input); let mac = hmac.finalize().into_bytes().to_vec(); @@ -368,7 +375,7 @@ impl Vault { &mut self, mac_input: Vec, expected_mac: Vec, - key_version: i32, + key_version: RootKeyHistoryId, ) -> Result { let Unsealed { root_key, @@ -385,7 +392,7 @@ impl Vault { Ok(v) => v, Err(_) => unreachable!("HMAC accepts keys of any size"), }); - hmac.update(&key_version.to_be_bytes()); + hmac.update(&key_version.to_raw().to_be_bytes()); hmac.update(&mac_input); Ok(hmac.verify_slice(&expected_mac).is_ok()) @@ -408,7 +415,7 @@ impl Vault { #[cfg(test)] mod tests { - use crate::actors::GlobalActors; + use crate::{actors::GlobalActors, db::models::RootKeyHistory}; use arbiter_crypto::safecell::SafeCellHandle as _; use super::*; @@ -444,8 +451,8 @@ mod tests { assert!(n2.to_vec() > n1.to_vec(), "nonce must increase"); let mut conn = db.get().await.unwrap(); - let root_row: models::RootKeyHistory = schema::root_key_history::table - .select(models::RootKeyHistory::as_select()) + let root_row: RootKeyHistory = schema::root_key_history::table + .select(RootKeyHistory::as_select()) .first(&mut conn) .await .unwrap(); diff --git a/server/crates/arbiter-server/src/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index 28f0b9c..17fc7b0 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -79,10 +79,41 @@ pub mod types { } } - #[derive(Debug, FromSqlRow, AsExpression, Clone)] - #[diesel(sql_type = Integer)] - #[repr(transparent)] // hint compiler to optimize the wrapper struct away - pub struct ChainId(pub i32); + macro_rules! declare_id { + ($name:ident) => { + #[derive(Debug, FromSqlRow, AsExpression, Clone, Hash, Copy, PartialEq, Eq)] + #[diesel(sql_type = Integer)] + #[repr(transparent)] // hint compiler to optimize the wrapper struct away + pub struct $name(i32); + + impl $name { + pub const fn to_raw(self) -> i32 { + self.0 + } + pub const fn from_raw(raw: i32) -> Self { + Self(raw) + } + } + + impl FromSql for $name { + fn from_sql( + bytes: ::RawValue<'_>, + ) -> diesel::deserialize::Result { + FromSql::::from_sql(bytes).map(Self) + } + } + impl ToSql for $name { + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, Sqlite>, + ) -> diesel::serialize::Result { + ToSql::::to_sql(&self.0, out) + } + } + }; + } + + declare_id!(ChainId); #[expect( clippy::cast_sign_loss, @@ -103,21 +134,13 @@ pub mod types { } }; - impl FromSql for ChainId { - fn from_sql( - bytes: ::RawValue<'_>, - ) -> diesel::deserialize::Result { - FromSql::::from_sql(bytes).map(Self) - } - } - impl ToSql for ChainId { - fn to_sql<'b>( - &'b self, - out: &mut diesel::serialize::Output<'b, '_, Sqlite>, - ) -> diesel::serialize::Result { - ToSql::::to_sql(&self.0, out) - } - } + declare_id!(OperatorId); + declare_id!(OperatorIdentityId); + declare_id!(AeadEncryptedId); + declare_id!(RootKeyHistoryId); + declare_id!(TlsHistoryId); + declare_id!(EvmWalletId); + declare_id!(ClientId); } pub use types::*; @@ -130,12 +153,12 @@ pub use types::*; )] #[diesel(table_name = aead_encrypted, check_for_backend(Sqlite))] pub struct AeadEncrypted { - pub id: i32, + pub id: AeadEncryptedId, pub ciphertext: Vec, pub tag: Vec, pub current_nonce: Vec, pub schema_version: i32, - pub associated_root_key_id: i32, // references root_key_history.id + pub associated_root_key_id: RootKeyHistoryId, pub created_at: SqliteTimestamp, } @@ -148,7 +171,7 @@ pub struct AeadEncrypted { attributes_with = "deriveless" )] pub struct RootKeyHistory { - pub id: i32, + pub id: RootKeyHistoryId, pub ciphertext: Vec, pub tag: Vec, pub root_key_encryption_nonce: Vec, @@ -166,7 +189,7 @@ pub struct RootKeyHistory { attributes_with = "deriveless" )] pub struct TlsHistory { - pub id: i32, + pub id: TlsHistoryId, pub cert: String, pub cert_key: String, // PEM Encoded private key pub ca_cert: String, // PEM Encoded certificate for cert signing @@ -191,7 +214,7 @@ pub struct ArbiterSettings { attributes_with = "deriveless" )] pub struct EvmWallet { - pub id: i32, + pub id: EvmWalletId, pub address: Vec, pub aead_encrypted_id: i32, pub created_at: SqliteTimestamp, @@ -213,7 +236,7 @@ pub struct EvmWallet { )] pub struct EvmWalletAccess { pub id: i32, - pub wallet_id: i32, + pub wallet_id: EvmWalletId, pub client_id: i32, pub created_at: SqliteTimestamp, } @@ -240,7 +263,7 @@ pub struct ProgramClientMetadataHistory { #[derive(Models, Queryable, Debug, Insertable, Selectable)] #[diesel(table_name = schema::program_client, check_for_backend(Sqlite))] pub struct ProgramClient { - pub id: i32, + pub id: ClientId, pub public_key: Vec, pub metadata_id: i32, pub created_at: SqliteTimestamp, @@ -250,7 +273,7 @@ pub struct ProgramClient { #[derive(Queryable, Debug)] #[diesel(table_name = schema::operator_identity, check_for_backend(Sqlite))] pub struct OperatorIdentity { - pub id: i32, + pub id: OperatorIdentityId, pub public_key: Vec, pub created_at: SqliteTimestamp, pub updated_at: SqliteTimestamp, @@ -259,14 +282,13 @@ pub struct OperatorIdentity { #[derive(Queryable, Debug)] #[diesel(table_name = schema::operator, check_for_backend(Sqlite))] pub struct Operator { - pub id: i32, + pub id: OperatorId, pub share: Vec, pub share_nonce: Vec, pub created_at: SqliteTimestamp, pub updated_at: SqliteTimestamp, } - #[derive(Models, Queryable, Debug, Insertable, Selectable)] #[diesel(table_name = evm_ether_transfer_limit, check_for_backend(Sqlite))] #[view( @@ -410,7 +432,7 @@ pub struct IntegrityEnvelope { pub entity_kind: String, pub entity_id: Vec, pub payload_version: i32, - pub key_version: i32, + pub key_version: RootKeyHistoryId, pub mac: Vec, pub signed_at: SqliteTimestamp, pub created_at: SqliteTimestamp, diff --git a/server/crates/arbiter-server/src/evm/mod.rs b/server/crates/arbiter-server/src/evm/mod.rs index 44c581d..716a360 100644 --- a/server/crates/arbiter-server/src/evm/mod.rs +++ b/server/crates/arbiter-server/src/evm/mod.rs @@ -363,7 +363,8 @@ mod tests { use crate::db::{ self, DatabaseConnection, models::{ - EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp, + EvmBasicGrant, EvmWalletAccess, EvmWalletId, NewEvmBasicGrant, NewEvmTransactionLog, + SqliteTimestamp, }, schema::{evm_basic_grant, evm_transaction_log}, }; @@ -381,7 +382,7 @@ mod tests { EvalContext { target: EvmWalletAccess { id: WALLET_ACCESS_ID, - wallet_id: 10, + wallet_id: EvmWalletId::from_raw(5), client_id: 20, created_at: SqliteTimestamp(Utc::now()), }, 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 80bd3ba..b9deb99 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 @@ -3,7 +3,8 @@ use crate::{ db::{ self, DatabaseConnection, models::{ - EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp, + EvmBasicGrant, EvmWalletAccess, EvmWalletId, NewEvmBasicGrant, NewEvmTransactionLog, + SqliteTimestamp, }, schema::{evm_basic_grant, evm_transaction_log}, }, @@ -31,7 +32,7 @@ fn ctx(to: Address, value: U256) -> EvalContext { EvalContext { target: EvmWalletAccess { id: WALLET_ACCESS_ID, - wallet_id: 10, + wallet_id: EvmWalletId::from_raw(10), client_id: 20, created_at: SqliteTimestamp(Utc::now()), }, 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 0ea8c9c..f2c02b3 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 @@ -2,7 +2,7 @@ use super::{Settings, TokenTransfer}; use crate::{ db::{ self, DatabaseConnection, - models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp}, + models::{EvmBasicGrant, EvmWalletAccess, EvmWalletId, NewEvmBasicGrant, SqliteTimestamp}, schema::evm_basic_grant, }, evm::{ @@ -45,7 +45,7 @@ fn ctx(to: Address, calldata: Bytes) -> EvalContext { EvalContext { target: EvmWalletAccess { id: WALLET_ACCESS_ID, - wallet_id: 10, + wallet_id: EvmWalletId::from_raw(10), client_id: 20, created_at: SqliteTimestamp(Utc::now()), }, diff --git a/server/crates/arbiter-server/src/grpc/operator/evm.rs b/server/crates/arbiter-server/src/grpc/operator/evm.rs index 0b3ac2c..9578c6e 100644 --- a/server/crates/arbiter-server/src/grpc/operator/evm.rs +++ b/server/crates/arbiter-server/src/grpc/operator/evm.rs @@ -90,7 +90,7 @@ async fn handle_wallet_list( .into_iter() .map(|(id, address)| WalletEntry { address: address.to_vec(), - id, + id: id.to_raw(), }) .collect(), }), diff --git a/server/crates/arbiter-server/src/grpc/operator/inbound.rs b/server/crates/arbiter-server/src/grpc/operator/inbound.rs index 7e2c87e..dc62d4f 100644 --- a/server/crates/arbiter-server/src/grpc/operator/inbound.rs +++ b/server/crates/arbiter-server/src/grpc/operator/inbound.rs @@ -1,11 +1,10 @@ use crate::{ - db::models::{CoreEvmWalletAccess, NewEvmWalletAccess}, + db::models::{CoreEvmWalletAccess, EvmWalletId, NewEvmWalletAccess}, evm::policies::{ SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, ether_transfer, token_transfers, }, - grpc::Convert, - grpc::TryConvert, + grpc::{Convert, TryConvert}, }; use arbiter_proto::{ proto::evm::{ @@ -150,7 +149,7 @@ impl Convert for WalletAccess { fn convert(self) -> Self::Output { NewEvmWalletAccess { - wallet_id: self.wallet_id, + wallet_id: EvmWalletId::from_raw(self.wallet_id), client_id: self.sdk_client_id, } } @@ -165,7 +164,7 @@ impl TryConvert for SdkClientWalletAccess { return Err(Status::invalid_argument("Missing wallet access entry")); }; Ok(CoreEvmWalletAccess { - wallet_id: access.wallet_id, + wallet_id: EvmWalletId::from_raw(access.wallet_id), client_id: access.sdk_client_id, id: self.id, }) diff --git a/server/crates/arbiter-server/src/grpc/operator/outbound.rs b/server/crates/arbiter-server/src/grpc/operator/outbound.rs index a4f4c29..c10764b 100644 --- a/server/crates/arbiter-server/src/grpc/operator/outbound.rs +++ b/server/crates/arbiter-server/src/grpc/operator/outbound.rs @@ -1,5 +1,5 @@ use crate::{ - db::models::EvmWalletAccess, + db::models::{EvmWalletAccess, EvmWalletId}, evm::policies::{SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit}, grpc::Convert, }; @@ -103,7 +103,7 @@ impl Convert for EvmWalletAccess { Self::Output { id: self.id, access: Some(WalletAccess { - wallet_id: self.wallet_id, + wallet_id: self.wallet_id.to_raw(), sdk_client_id: self.client_id, }), } diff --git a/server/crates/arbiter-server/src/grpc/operator/sdk_client.rs b/server/crates/arbiter-server/src/grpc/operator/sdk_client.rs index b88a73e..e8864db 100644 --- a/server/crates/arbiter-server/src/grpc/operator/sdk_client.rs +++ b/server/crates/arbiter-server/src/grpc/operator/sdk_client.rs @@ -1,8 +1,8 @@ use crate::{ - db::models::NewEvmWalletAccess, + db::models::{ClientId, NewEvmWalletAccess}, grpc::Convert, peers::operator::{ - OutOfBand, OperatorSession, + OperatorSession, OutOfBand, session::handlers::{ HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove, HandleRevokeEvmWalletAccess, HandleSdkClientList, @@ -11,8 +11,8 @@ use crate::{ }; use arbiter_crypto::authn; use arbiter_proto::proto::{ - shared::ClientInfo as ProtoClientMetadata, operator::{ + operator_response::Payload as OperatorResponsePayload, sdk_client::{ self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel, ConnectionRequest as ProtoSdkClientConnectionRequest, @@ -24,8 +24,8 @@ use arbiter_proto::proto::{ request::Payload as SdkClientRequestPayload, response::Payload as SdkClientResponsePayload, }, - operator_response::Payload as OperatorResponsePayload, }, + shared::ClientInfo as ProtoClientMetadata, }; use kameo::actor::ActorRef; @@ -115,7 +115,7 @@ async fn handle_list( clients: clients .into_iter() .map(|(client, metadata)| ProtoSdkClientEntry { - id: client.id, + id: client.id.to_raw(), pubkey: client.public_key.clone(), info: Some(ProtoClientMetadata { name: metadata.name, diff --git a/server/crates/arbiter-server/src/peers/operator/session/handlers.rs b/server/crates/arbiter-server/src/peers/operator/session/handlers.rs index 86a422e..a2ccf0f 100644 --- a/server/crates/arbiter-server/src/peers/operator/session/handlers.rs +++ b/server/crates/arbiter-server/src/peers/operator/session/handlers.rs @@ -1,12 +1,16 @@ use super::{Error, OperatorSession}; use crate::{ - actors::evm::{ - ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError, - OperatorCreateGrant, OperatorListGrants, + actors::{ + evm::{ + ClientSignTransaction, Generate, ListWallets, OperatorCreateGrant, OperatorListGrants, + SignTransactionError as EvmSignError, + }, + flow_coordinator::client_connect_approval::ClientApprovalAnswer, + vault::VaultState, + }, + db::models::{ + EvmWalletAccess, EvmWalletId, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata, }, - actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer, - actors::vault::VaultState, - db::models::{EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata}, evm::policies::{Grant, SpecificGrant}, }; use arbiter_crypto::authn; @@ -70,7 +74,9 @@ impl OperatorSession { } #[message] - pub(crate) async fn handle_evm_wallet_list(&mut self) -> Result, Error> { + pub(crate) async fn handle_evm_wallet_list( + &mut self, + ) -> Result, Error> { match self.props.actors.evm.ask(ListWallets {}).await { Ok(wallets) => Ok(wallets), Err(err) => {