Compare commits

..

2 Commits

Author SHA1 Message Date
Clippy Bot
cc21036448 fix(clippy): apply auto-fixable linting suggestions 2026-06-23 18:47:23 +00:00
CleverWild
4fd75701c7 ci(clippy): add auto-fix bot pipeline 2026-06-23 20:41:41 +02:00
439 changed files with 79190 additions and 79492 deletions

View File

@@ -0,0 +1,45 @@
when:
- event: push
branch: main
path:
include: ['.woodpecker/server-*.yaml', 'server/**']
steps:
- name: clippy-fix
image: jdxcode/mise:latest
directory: server
environment:
CARGO_TERM_COLOR: always
CARGO_TARGET_DIR: /usr/local/cargo/target
CARGO_HOME: /usr/local/cargo/registry
GITEA_TOKEN:
from_secret: GITEA_TOKEN
GITEA_URL:
from_secret: GITEA_URL
volumes:
- cargo-target:/usr/local/cargo/target
- cargo-registry:/usr/local/cargo/registry
commands:
- apt-get update && apt-get install -y pkg-config curl
- mise install rust
- mise install protoc
- mise exec rust -- cargo clippy --fix --allow-dirty --all
- |
cd ..
if git diff --quiet HEAD; then
echo "No machine-applicable clippy fixes found, nothing to do."
exit 0
fi
git config user.email "bot@arbiter"
git config user.name "Clippy Bot"
git add -A
git commit -m "fix(clippy): apply auto-fixable linting suggestions"
git config http.extraHeader "Authorization: token ${GITEA_TOKEN}"
git push --force origin HEAD:refs/heads/bot/clippy-fixes
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "${GITEA_URL}/api/v1/repos/${CI_REPO}/pulls" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"title\":\"fix(clippy): apply auto-fixable linting suggestions\",\"head\":\"bot/clippy-fixes\",\"base\":\"main\",\"body\":\"Automated clippy fixes generated by CI bot.\"}")
echo "Gitea API response: ${HTTP_STATUS}"
[ "$HTTP_STATUS" = "201" ] || [ "$HTTP_STATUS" = "409" ] || [ "$HTTP_STATUS" = "422" ] || exit 1

61
server/Cargo.lock generated
View File

@@ -719,7 +719,6 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"trybuild",
]
[[package]]
@@ -1966,12 +1965,6 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "dissimilar"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e"
[[package]]
name = "downcast-rs"
version = "2.0.2"
@@ -3207,7 +3200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d"
dependencies = [
"serde",
"toml 0.9.12+spec-1.1.0",
"toml",
]
[[package]]
@@ -4979,12 +4972,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "target-triple"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b"
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -4998,15 +4985,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.4.4"
@@ -5229,21 +5207,6 @@ dependencies = [
"winnow 0.7.15",
]
[[package]]
name = "toml"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
dependencies = [
"indexmap 2.14.0",
"serde_core",
"serde_spanned",
"toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow 1.0.2",
]
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
@@ -5283,12 +5246,6 @@ dependencies = [
"winnow 1.0.2",
]
[[package]]
name = "toml_writer"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
[[package]]
name = "tonic"
version = "0.14.5"
@@ -5476,22 +5433,6 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "trybuild"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0710d4dfbeae4f9c390baa784c49858a7468fa433f3fe5d0ec5ebef651cf59f9"
dependencies = [
"dissimilar",
"glob",
"serde",
"serde_derive",
"serde_json",
"target-triple",
"termcolor",
"toml 1.1.2+spec-1.1.0",
]
[[package]]
name = "typenum"
version = "1.20.0"

View File

@@ -129,6 +129,7 @@ rc_buffer = "warn"
rc_mutex = "warn"
redundant_test_prefix = "warn"
redundant_type_annotations = "warn"
ref_patterns = "warn"
renamed_function_params = "warn"
rest_pat_in_fully_bound_structs = "warn"
return_and_then = "warn"

View File

@@ -1,49 +0,0 @@
use crate::hashing::Hashable;
/// Marks a struct as a participant in the database integrity system.
///
/// Implementors are protected by an HMAC-SHA256 MAC stored in the
/// `integrity_envelope` table. The MAC is computed over:
///
/// ```text
/// HMAC-SHA256(key, len(KIND) || KIND || len(entity_id) || entity_id || VERSION || SHA256(Hashable))
/// ```
///
/// Both `KIND` and `VERSION` act as domain separators — they prevent a valid
/// MAC for one entity type or schema version from being accepted for another.
///
/// # Deriving
///
/// Use `#[derive(Integrable)]` with the `#[integrable(kind = "...")]` attribute.
/// `VERSION` is computed automatically as an FNV-1a hash of the struct's field
/// names and types, so it changes whenever the schema changes without any manual
/// bookkeeping.
///
/// ```rust,ignore
/// #[derive(Hashable, Integrable)]
/// #[integrable(kind = "operator_credentials")]
/// pub struct OperatorCredentials {
/// pub pubkey: PublicKey,
/// }
/// ```
///
/// # Upgrading schema
///
/// When fields are added, removed, or reordered, `VERSION` changes automatically.
/// Existing MAC records in the database will return [`PayloadVersionMismatch`] on
/// verification — this is the signal to re-sign all rows for this `KIND` as part
/// of a migration.
///
/// [`PayloadVersionMismatch`]: crate::integrity::Integrable
pub trait Integrable: Hashable {
/// Stable name of this entity type as stored in `integrity_envelope.entity_kind`.
///
/// Must be a valid schema name: starts with a letter, contains only `[a-zA-Z0-9_]`,
/// and must be globally unique across all `Integrable` types in the system.
const KIND: &'static str;
/// FNV-1a hash of the struct's field names and types at the time the derive
/// macro ran. Changes automatically when the schema changes, invalidating
/// existing MACs and signalling that a migration is required.
const VERSION: i32;
}

View File

@@ -1,7 +1,6 @@
#[cfg(feature = "authn")]
pub mod authn;
pub mod hashing;
pub mod integrity;
#[cfg(feature = "safecell")]
pub mod safecell;

View File

@@ -14,7 +14,6 @@ syn = { version = "2.0", features = ["derive", "fold", "full", "visit-mut"] }
[dev-dependencies]
arbiter-crypto = { path = "../arbiter-crypto" }
trybuild = { version = "1.0", features = ["diff"] }
[lints]
workspace = true

View File

@@ -53,16 +53,32 @@ struct FieldAccess {
fn collect_field_accesses(struct_data: &DataStruct) -> Vec<FieldAccess> {
match &struct_data.fields {
Fields::Named(fields) => crate::utils::sorted_named_fields(fields)
.into_iter()
.map(|field| {
let name = field.ident.as_ref().unwrap();
FieldAccess {
Fields::Named(fields) => {
// Keep deterministic alphabetical order for named fields.
// Do not remove this sort, because it keeps hash output stable regardless of source order.
let mut named_fields = fields
.named
.iter()
.map(|field| {
let name = field
.ident
.as_ref()
.expect("Fields::Named(fields) must have names")
.clone();
(name.to_string(), name)
})
.collect::<Vec<_>>();
named_fields.sort_by(|a, b| a.0.cmp(&b.0));
named_fields
.into_iter()
.map(|(_, name)| FieldAccess {
access: quote! { #name },
span: name.span(),
}
})
.collect(),
})
.collect()
}
Fields::Unnamed(fields) => fields
.unnamed
.iter()

View File

@@ -1,134 +0,0 @@
use crate::utils::INTEGRABLE_TRAIT_PATH;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, LitStr, spanned::Spanned as _};
struct IntegrableAttr {
kind: String,
}
impl IntegrableAttr {
fn from_attrs(
attrs: &[syn::Attribute],
ident_span: proc_macro2::Span,
) -> Result<Self, syn::Error> {
let mut kind: Option<String> = None;
let mut found = false;
for attr in attrs {
if !attr.path().is_ident("integrable") {
continue;
}
if found {
return Err(syn::Error::new(attr.span(), "duplicate #[integrable] attribute"));
}
found = true;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("kind") {
let lit: LitStr = meta.value()?.parse()?;
let v = lit.value();
if v.is_empty() {
return Err(syn::Error::new(lit.span(), "kind must not be empty"));
}
if !is_valid_kind(&v) {
return Err(syn::Error::new(
lit.span(),
"kind must be a valid schema name: start with a letter, contain only [a-zA-Z0-9_]",
));
}
kind = Some(v);
} else {
return Err(meta.error("unknown key; expected `kind`"));
}
Ok(())
})?;
}
let kind = kind.ok_or_else(|| {
syn::Error::new(ident_span, "#[integrable(kind = \"...\")] is required")
})?;
Ok(Self { kind })
}
}
fn is_valid_kind(s: &str) -> bool {
let mut chars = s.chars();
matches!(chars.next(), Some(c) if c.is_ascii_alphabetic())
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn fnv1a(data: &[u8], mut hash: u32) -> u32 {
const FNV_PRIME: u32 = 16_777_619;
for &b in data {
hash ^= u32::from(b);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
// Hashes field names and types using the same alphabetical sort order as Hashable,
// so that source-level field reordering never changes VERSION.
fn compute_version(fields: &syn::Fields) -> i32 {
const FNV_OFFSET: u32 = 2_166_136_261;
let mut hash = FNV_OFFSET;
match fields {
syn::Fields::Named(named) => {
for field in crate::utils::sorted_named_fields(named) {
let name = field.ident.as_ref().unwrap().to_string();
let ty = &field.ty;
hash = fnv1a(name.as_bytes(), hash);
hash = fnv1a(quote!(#ty).to_string().as_bytes(), hash);
}
}
syn::Fields::Unnamed(unnamed) => {
for (i, field) in unnamed.unnamed.iter().enumerate() {
let ty = &field.ty;
hash = fnv1a(i.to_string().as_bytes(), hash);
hash = fnv1a(quote!(#ty).to_string().as_bytes(), hash);
}
}
syn::Fields::Unit => {}
}
// Clear sign bit to guarantee a positive i32; substitute 0 → 1.
let v = (hash >> 1).cast_signed();
if v == 0 { 1 } else { v }
}
pub(crate) fn derive(input: &DeriveInput) -> TokenStream {
let syn::Data::Struct(ref data) = input.data else {
return syn::Error::new(
input.ident.span(),
"#[derive(Integrable)] is only supported on structs",
)
.to_compile_error();
};
let integrable_trait = INTEGRABLE_TRAIT_PATH.to_path();
let hashable_trait = crate::utils::HASHABLE_TRAIT_PATH.to_path();
let ident = &input.ident;
let mut generics = input.generics.clone();
for type_param in generics.type_params_mut() {
type_param.bounds.push(syn::parse_quote!(#hashable_trait));
}
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let attr = match IntegrableAttr::from_attrs(&input.attrs, input.ident.span()) {
Ok(a) => a,
Err(e) => return e.to_compile_error(),
};
let kind = attr.kind;
let version = compute_version(&data.fields);
quote! {
#[automatically_derived]
impl #impl_generics #integrable_trait for #ident #ty_generics #where_clause {
const KIND: &'static str = #kind;
const VERSION: i32 = #version;
}
}
}

View File

@@ -1,7 +1,6 @@
use syn::{DeriveInput, parse_macro_input};
mod hashable;
mod integrable;
mod utils;
#[proc_macro_derive(Hashable)]
@@ -9,9 +8,3 @@ pub fn derive_hashable(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
let input = parse_macro_input!(input as DeriveInput);
hashable::derive(&input).into()
}
#[proc_macro_derive(Integrable, attributes(integrable))]
pub fn derive_integrable(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
integrable::derive(&input).into()
}

View File

@@ -22,14 +22,3 @@ macro_rules! ensure_path {
ensure_path!(::arbiter_crypto::hashing::Hashable as HASHABLE_TRAIT_PATH);
ensure_path!(::arbiter_crypto::hashing::Digest as HMAC_DIGEST_PATH);
ensure_path!(::arbiter_crypto::integrity::Integrable as INTEGRABLE_TRAIT_PATH);
/// Returns named struct fields sorted alphabetically by name.
/// Both `Hashable` and `Integrable` derive macros must iterate fields in the
/// same deterministic order so that source-level reordering never changes
/// either the runtime hash or the compile-time VERSION.
pub(crate) fn sorted_named_fields(fields: &syn::FieldsNamed) -> Vec<&syn::Field> {
let mut v: Vec<&syn::Field> = fields.named.iter().collect();
v.sort_by_key(|f| f.ident.as_ref().unwrap().to_string());
v
}

View File

@@ -1,53 +0,0 @@
use arbiter_crypto::integrity::Integrable;
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "test_entity")]
struct TestEntity {
value: i32,
}
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "other_entity")]
struct OtherEntity {
label: String,
count: u64,
}
#[test]
fn kind_is_set_correctly() {
assert_eq!(<TestEntity as Integrable>::KIND, "test_entity");
assert_eq!(<OtherEntity as Integrable>::KIND, "other_entity");
}
#[test]
fn version_is_positive() {
const {
assert!(<TestEntity as Integrable>::VERSION > 0);
assert!(<OtherEntity as Integrable>::VERSION > 0);
}
}
#[test]
fn different_field_layouts_produce_different_versions() {
assert_ne!(
<TestEntity as Integrable>::VERSION,
<OtherEntity as Integrable>::VERSION,
);
}
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "generic_entity")]
struct GenericEntity<T> {
inner: T,
}
#[test]
fn generic_struct_derives_integrable() {
assert_eq!(
<GenericEntity<TestEntity> as Integrable>::KIND,
"generic_entity"
);
const {
assert!(<GenericEntity<TestEntity> as Integrable>::VERSION > 0);
}
}

View File

@@ -1,5 +0,0 @@
#[test]
fn integrable_compile_fail() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/integrable/*.rs");
}

View File

@@ -1,8 +0,0 @@
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "entity_a")]
#[integrable(kind = "entity_b")]
struct DuplicateAttr {
value: i32,
}
fn main() {}

View File

@@ -1,5 +0,0 @@
error: duplicate #[integrable] attribute
--> tests/ui/integrable/duplicate_attr.rs:3:1
|
3 | #[integrable(kind = "entity_b")]
| ^

View File

@@ -1,7 +0,0 @@
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "")]
struct EmptyKind {
value: i32,
}
fn main() {}

View File

@@ -1,5 +0,0 @@
error: kind must not be empty
--> tests/ui/integrable/empty_kind.rs:2:21
|
2 | #[integrable(kind = "")]
| ^^

View File

@@ -1,8 +0,0 @@
#[derive(arbiter_macros::Integrable)]
#[integrable(kind = "my_enum")]
enum MyEnum {
A,
B,
}
fn main() {}

View File

@@ -1,5 +0,0 @@
error: #[derive(Integrable)] is only supported on structs
--> tests/ui/integrable/enum_not_supported.rs:3:6
|
3 | enum MyEnum {
| ^^^^^^

View File

@@ -1,7 +0,0 @@
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "bad kind!")]
struct InvalidKind {
value: i32,
}
fn main() {}

View File

@@ -1,5 +0,0 @@
error: kind must be a valid schema name: start with a letter, contain only [a-zA-Z0-9_]
--> tests/ui/integrable/invalid_kind.rs:2:21
|
2 | #[integrable(kind = "bad kind!")]
| ^^^^^^^^^^^

View File

@@ -1,6 +0,0 @@
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
struct MissingAttr {
value: i32,
}
fn main() {}

View File

@@ -1,5 +0,0 @@
error: #[integrable(kind = "...")] is required
--> tests/ui/integrable/missing_attr.rs:2:8
|
2 | struct MissingAttr {
| ^^^^^^^^^^^

View File

@@ -52,7 +52,10 @@ pub const INTEGRITY_SUBKEY_TAG: &[u8] = b"arbiter/db-integrity-key/v1";
pub type HmacSha256 = Hmac<Sha256>;
pub use arbiter_crypto::integrity::Integrable;
pub trait Integrable: Hashable {
const KIND: &'static str;
const VERSION: i32 = 1;
}
fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
let mut hasher = Sha256::new();
@@ -214,13 +217,15 @@ mod tests {
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use super::{Error, sign_entity, verify_entity};
#[derive(Clone, arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "dummy_entity")]
use super::{Error, Integrable, sign_entity, verify_entity};
#[derive(Clone, arbiter_macros::Hashable)]
struct DummyEntity {
payload_version: i32,
payload: Vec<u8>,
}
impl Integrable for DummyEntity {
const KIND: &'static str = "dummy_entity";
}
async fn bootstrapped_vault(db: &db::DatabasePool) -> ActorRef<Vault> {
let actor = Vault::spawn(

View File

@@ -1,5 +1,6 @@
use super::{DatabaseID, EvalContext, EvalViolation};
use crate::{
crypto::integrity::v1::Integrable,
db::models::{
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit,
NewEvmEtherTransferLimit, SqliteTimestamp,
@@ -51,12 +52,14 @@ impl From<Meaning> for SpecificMeaning {
}
// A grant for ether transfers, which can be scoped to specific target addresses and volume limits
#[derive(Debug, Clone, arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "EtherTransfer")]
#[derive(Debug, Clone, arbiter_macros::Hashable)]
pub struct Settings {
pub target: Vec<Address>,
pub limit: VolumeRateLimit,
}
impl Integrable for Settings {
const KIND: &'static str = "EtherTransfer";
}
impl From<Settings> for SpecificGrant {
fn from(val: Settings) -> Self {

View File

@@ -1,5 +1,6 @@
use super::{DatabaseID, EvalContext, EvalViolation};
use crate::{
crypto::integrity::Integrable,
db::models::{
EvmBasicGrant, EvmTokenTransferGrant, EvmTokenTransferVolumeLimit,
NewEvmTokenTransferGrant, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit,
@@ -62,13 +63,15 @@ impl From<Meaning> for SpecificMeaning {
}
// A grant for token transfers, which can be scoped to specific target addresses and volume limits
#[derive(Debug, Clone, arbiter_macros::Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "TokenTransfer")]
#[derive(Debug, Clone, arbiter_macros::Hashable)]
pub struct Settings {
pub token_contract: Address,
pub target: Option<Address>,
pub volume_limits: Vec<VolumeRateLimit>,
}
impl Integrable for Settings {
const KIND: &'static str = "TokenTransfer";
}
impl From<Settings> for SpecificGrant {
fn from(val: Settings) -> Self {

View File

@@ -1,5 +1,5 @@
use crate::{
actors::GlobalActors, db, peers::client::session::ClientSession,
actors::GlobalActors, crypto::integrity::Integrable, db, peers::client::session::ClientSession,
};
use arbiter_crypto::authn;
use arbiter_macros::Hashable;
@@ -14,12 +14,15 @@ pub struct ClientProfile {
pub metadata: ClientMetadata,
}
#[derive(Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "client_credentials")]
#[derive(Hashable)]
pub struct ClientCredentials {
pub pubkey: authn::PublicKey,
}
impl Integrable for ClientCredentials {
const KIND: &'static str = "client_credentials";
}
pub struct ClientConnection {
pub(crate) db: db::DatabasePool,
pub(crate) actors: GlobalActors,

View File

@@ -3,7 +3,7 @@ use crate::{
GlobalActors,
vault::{GetState, Vault},
},
crypto::integrity::{self, AttestationStatus},
crypto::integrity::{self, AttestationStatus, Integrable},
db::{DatabaseError, DatabasePool},
peers::client::ClientProfile,
};
@@ -23,13 +23,16 @@ pub mod auth;
pub mod session;
pub mod vault_gate;
#[derive(Debug, Clone, Hashable, arbiter_macros::Integrable)]
#[integrable(kind = "operator_credentials")]
#[derive(Debug, Clone, Hashable)]
pub struct Credentials {
pub id: i32,
pub pubkey: authn::PublicKey,
}
impl Integrable for Credentials {
const KIND: &'static str = "operator_credentials";
}
// Messages, sent by operator to connection client without having a request
#[derive(Debug)]
pub enum OutOfBand {