Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0779d0db13 | ||
|
|
11a2d8c8f3 | ||
|
|
d861ff80be | ||
|
|
647f0c8519 |
61
server/Cargo.lock
generated
61
server/Cargo.lock
generated
@@ -719,6 +719,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
|
"trybuild",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1965,6 +1966,12 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dissimilar"
|
||||||
|
version = "1.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "downcast-rs"
|
name = "downcast-rs"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@@ -3200,7 +3207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d"
|
checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"toml",
|
"toml 0.9.12+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4972,6 +4979,12 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "target-triple"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
@@ -4985,6 +4998,15 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "terminal_size"
|
name = "terminal_size"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@@ -5207,6 +5229,21 @@ dependencies = [
|
|||||||
"winnow 0.7.15",
|
"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]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.7.5+spec-1.1.0"
|
version = "0.7.5+spec-1.1.0"
|
||||||
@@ -5246,6 +5283,12 @@ dependencies = [
|
|||||||
"winnow 1.0.2",
|
"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]]
|
[[package]]
|
||||||
name = "tonic"
|
name = "tonic"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -5433,6 +5476,22 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
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]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.20.0"
|
version = "1.20.0"
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ rc_buffer = "warn"
|
|||||||
rc_mutex = "warn"
|
rc_mutex = "warn"
|
||||||
redundant_test_prefix = "warn"
|
redundant_test_prefix = "warn"
|
||||||
redundant_type_annotations = "warn"
|
redundant_type_annotations = "warn"
|
||||||
ref_patterns = "warn"
|
|
||||||
renamed_function_params = "warn"
|
renamed_function_params = "warn"
|
||||||
rest_pat_in_fully_bound_structs = "warn"
|
rest_pat_in_fully_bound_structs = "warn"
|
||||||
return_and_then = "warn"
|
return_and_then = "warn"
|
||||||
|
|||||||
49
server/crates/arbiter-crypto/src/integrity.rs
Normal file
49
server/crates/arbiter-crypto/src/integrity.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#[cfg(feature = "authn")]
|
#[cfg(feature = "authn")]
|
||||||
pub mod authn;
|
pub mod authn;
|
||||||
pub mod hashing;
|
pub mod hashing;
|
||||||
|
pub mod integrity;
|
||||||
#[cfg(feature = "safecell")]
|
#[cfg(feature = "safecell")]
|
||||||
pub mod safecell;
|
pub mod safecell;
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ syn = { version = "2.0", features = ["derive", "fold", "full", "visit-mut"] }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
arbiter-crypto = { path = "../arbiter-crypto" }
|
arbiter-crypto = { path = "../arbiter-crypto" }
|
||||||
|
trybuild = { version = "1.0", features = ["diff"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -53,32 +53,16 @@ struct FieldAccess {
|
|||||||
|
|
||||||
fn collect_field_accesses(struct_data: &DataStruct) -> Vec<FieldAccess> {
|
fn collect_field_accesses(struct_data: &DataStruct) -> Vec<FieldAccess> {
|
||||||
match &struct_data.fields {
|
match &struct_data.fields {
|
||||||
Fields::Named(fields) => {
|
Fields::Named(fields) => crate::utils::sorted_named_fields(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()
|
.into_iter()
|
||||||
.map(|(_, name)| FieldAccess {
|
.map(|field| {
|
||||||
|
let name = field.ident.as_ref().unwrap();
|
||||||
|
FieldAccess {
|
||||||
access: quote! { #name },
|
access: quote! { #name },
|
||||||
span: name.span(),
|
span: name.span(),
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
Fields::Unnamed(fields) => fields
|
Fields::Unnamed(fields) => fields
|
||||||
.unnamed
|
.unnamed
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
134
server/crates/arbiter-macros/src/integrable.rs
Normal file
134
server/crates/arbiter-macros/src/integrable.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
use syn::{DeriveInput, parse_macro_input};
|
use syn::{DeriveInput, parse_macro_input};
|
||||||
|
|
||||||
mod hashable;
|
mod hashable;
|
||||||
|
mod integrable;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
#[proc_macro_derive(Hashable)]
|
#[proc_macro_derive(Hashable)]
|
||||||
@@ -8,3 +9,9 @@ pub fn derive_hashable(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
|
|||||||
let input = parse_macro_input!(input as DeriveInput);
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
hashable::derive(&input).into()
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,3 +22,14 @@ macro_rules! ensure_path {
|
|||||||
|
|
||||||
ensure_path!(::arbiter_crypto::hashing::Hashable as HASHABLE_TRAIT_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::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
|
||||||
|
}
|
||||||
|
|||||||
53
server/crates/arbiter-macros/tests/integrable.rs
Normal file
53
server/crates/arbiter-macros/tests/integrable.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
server/crates/arbiter-macros/tests/integrable_errors.rs
Normal file
5
server/crates/arbiter-macros/tests/integrable_errors.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#[test]
|
||||||
|
fn integrable_compile_fail() {
|
||||||
|
let t = trybuild::TestCases::new();
|
||||||
|
t.compile_fail("tests/ui/integrable/*.rs");
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
|
||||||
|
#[integrable(kind = "entity_a")]
|
||||||
|
#[integrable(kind = "entity_b")]
|
||||||
|
struct DuplicateAttr {
|
||||||
|
value: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: duplicate #[integrable] attribute
|
||||||
|
--> tests/ui/integrable/duplicate_attr.rs:3:1
|
||||||
|
|
|
||||||
|
3 | #[integrable(kind = "entity_b")]
|
||||||
|
| ^
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
|
||||||
|
#[integrable(kind = "")]
|
||||||
|
struct EmptyKind {
|
||||||
|
value: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: kind must not be empty
|
||||||
|
--> tests/ui/integrable/empty_kind.rs:2:21
|
||||||
|
|
|
||||||
|
2 | #[integrable(kind = "")]
|
||||||
|
| ^^
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#[derive(arbiter_macros::Integrable)]
|
||||||
|
#[integrable(kind = "my_enum")]
|
||||||
|
enum MyEnum {
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: #[derive(Integrable)] is only supported on structs
|
||||||
|
--> tests/ui/integrable/enum_not_supported.rs:3:6
|
||||||
|
|
|
||||||
|
3 | enum MyEnum {
|
||||||
|
| ^^^^^^
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
|
||||||
|
#[integrable(kind = "bad kind!")]
|
||||||
|
struct InvalidKind {
|
||||||
|
value: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
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!")]
|
||||||
|
| ^^^^^^^^^^^
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#[derive(arbiter_macros::Hashable, arbiter_macros::Integrable)]
|
||||||
|
struct MissingAttr {
|
||||||
|
value: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: #[integrable(kind = "...")] is required
|
||||||
|
--> tests/ui/integrable/missing_attr.rs:2:8
|
||||||
|
|
|
||||||
|
2 | struct MissingAttr {
|
||||||
|
| ^^^^^^^^^^^
|
||||||
@@ -52,10 +52,7 @@ pub const INTEGRITY_SUBKEY_TAG: &[u8] = b"arbiter/db-integrity-key/v1";
|
|||||||
|
|
||||||
pub type HmacSha256 = Hmac<Sha256>;
|
pub type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
pub trait Integrable: Hashable {
|
pub use arbiter_crypto::integrity::Integrable;
|
||||||
const KIND: &'static str;
|
|
||||||
const VERSION: i32 = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
|
fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
@@ -217,15 +214,13 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
||||||
|
|
||||||
use super::{Error, Integrable, sign_entity, verify_entity};
|
use super::{Error, sign_entity, verify_entity};
|
||||||
#[derive(Clone, arbiter_macros::Hashable)]
|
#[derive(Clone, arbiter_macros::Hashable, arbiter_macros::Integrable)]
|
||||||
|
#[integrable(kind = "dummy_entity")]
|
||||||
struct DummyEntity {
|
struct DummyEntity {
|
||||||
payload_version: i32,
|
payload_version: i32,
|
||||||
payload: Vec<u8>,
|
payload: Vec<u8>,
|
||||||
}
|
}
|
||||||
impl Integrable for DummyEntity {
|
|
||||||
const KIND: &'static str = "dummy_entity";
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn bootstrapped_vault(db: &db::DatabasePool) -> ActorRef<Vault> {
|
async fn bootstrapped_vault(db: &db::DatabasePool) -> ActorRef<Vault> {
|
||||||
let actor = Vault::spawn(
|
let actor = Vault::spawn(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use super::{DatabaseID, EvalContext, EvalViolation};
|
use super::{DatabaseID, EvalContext, EvalViolation};
|
||||||
use crate::{
|
use crate::{
|
||||||
crypto::integrity::v1::Integrable,
|
|
||||||
db::models::{
|
db::models::{
|
||||||
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit,
|
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit,
|
||||||
NewEvmEtherTransferLimit, SqliteTimestamp,
|
NewEvmEtherTransferLimit, SqliteTimestamp,
|
||||||
@@ -52,14 +51,12 @@ impl From<Meaning> for SpecificMeaning {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A grant for ether transfers, which can be scoped to specific target addresses and volume limits
|
// A grant for ether transfers, which can be scoped to specific target addresses and volume limits
|
||||||
#[derive(Debug, Clone, arbiter_macros::Hashable)]
|
#[derive(Debug, Clone, arbiter_macros::Hashable, arbiter_macros::Integrable)]
|
||||||
|
#[integrable(kind = "EtherTransfer")]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub target: Vec<Address>,
|
pub target: Vec<Address>,
|
||||||
pub limit: VolumeRateLimit,
|
pub limit: VolumeRateLimit,
|
||||||
}
|
}
|
||||||
impl Integrable for Settings {
|
|
||||||
const KIND: &'static str = "EtherTransfer";
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Settings> for SpecificGrant {
|
impl From<Settings> for SpecificGrant {
|
||||||
fn from(val: Settings) -> Self {
|
fn from(val: Settings) -> Self {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use super::{DatabaseID, EvalContext, EvalViolation};
|
use super::{DatabaseID, EvalContext, EvalViolation};
|
||||||
use crate::{
|
use crate::{
|
||||||
crypto::integrity::Integrable,
|
|
||||||
db::models::{
|
db::models::{
|
||||||
EvmBasicGrant, EvmTokenTransferGrant, EvmTokenTransferVolumeLimit,
|
EvmBasicGrant, EvmTokenTransferGrant, EvmTokenTransferVolumeLimit,
|
||||||
NewEvmTokenTransferGrant, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit,
|
NewEvmTokenTransferGrant, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit,
|
||||||
@@ -63,15 +62,13 @@ impl From<Meaning> for SpecificMeaning {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A grant for token transfers, which can be scoped to specific target addresses and volume limits
|
// A grant for token transfers, which can be scoped to specific target addresses and volume limits
|
||||||
#[derive(Debug, Clone, arbiter_macros::Hashable)]
|
#[derive(Debug, Clone, arbiter_macros::Hashable, arbiter_macros::Integrable)]
|
||||||
|
#[integrable(kind = "TokenTransfer")]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub token_contract: Address,
|
pub token_contract: Address,
|
||||||
pub target: Option<Address>,
|
pub target: Option<Address>,
|
||||||
pub volume_limits: Vec<VolumeRateLimit>,
|
pub volume_limits: Vec<VolumeRateLimit>,
|
||||||
}
|
}
|
||||||
impl Integrable for Settings {
|
|
||||||
const KIND: &'static str = "TokenTransfer";
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Settings> for SpecificGrant {
|
impl From<Settings> for SpecificGrant {
|
||||||
fn from(val: Settings) -> Self {
|
fn from(val: Settings) -> Self {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
actors::GlobalActors, crypto::integrity::Integrable, db, peers::client::session::ClientSession,
|
actors::GlobalActors, db, peers::client::session::ClientSession,
|
||||||
};
|
};
|
||||||
use arbiter_crypto::authn;
|
use arbiter_crypto::authn;
|
||||||
use arbiter_macros::Hashable;
|
use arbiter_macros::Hashable;
|
||||||
@@ -14,15 +14,12 @@ pub struct ClientProfile {
|
|||||||
pub metadata: ClientMetadata,
|
pub metadata: ClientMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Hashable)]
|
#[derive(Hashable, arbiter_macros::Integrable)]
|
||||||
|
#[integrable(kind = "client_credentials")]
|
||||||
pub struct ClientCredentials {
|
pub struct ClientCredentials {
|
||||||
pub pubkey: authn::PublicKey,
|
pub pubkey: authn::PublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Integrable for ClientCredentials {
|
|
||||||
const KIND: &'static str = "client_credentials";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ClientConnection {
|
pub struct ClientConnection {
|
||||||
pub(crate) db: db::DatabasePool,
|
pub(crate) db: db::DatabasePool,
|
||||||
pub(crate) actors: GlobalActors,
|
pub(crate) actors: GlobalActors,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
GlobalActors,
|
GlobalActors,
|
||||||
vault::{GetState, Vault},
|
vault::{GetState, Vault},
|
||||||
},
|
},
|
||||||
crypto::integrity::{self, AttestationStatus, Integrable},
|
crypto::integrity::{self, AttestationStatus},
|
||||||
db::{DatabaseError, DatabasePool},
|
db::{DatabaseError, DatabasePool},
|
||||||
peers::client::ClientProfile,
|
peers::client::ClientProfile,
|
||||||
};
|
};
|
||||||
@@ -23,16 +23,13 @@ pub mod auth;
|
|||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod vault_gate;
|
pub mod vault_gate;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hashable)]
|
#[derive(Debug, Clone, Hashable, arbiter_macros::Integrable)]
|
||||||
|
#[integrable(kind = "operator_credentials")]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub pubkey: authn::PublicKey,
|
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
|
// Messages, sent by operator to connection client without having a request
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum OutOfBand {
|
pub enum OutOfBand {
|
||||||
|
|||||||
Reference in New Issue
Block a user