tests(server): property-based testing for ordering independency for hash
This commit is contained in:
5
server/Cargo.lock
generated
5
server/Cargo.lock
generated
@@ -745,6 +745,7 @@ dependencies = [
|
|||||||
"memsafe",
|
"memsafe",
|
||||||
"mutants",
|
"mutants",
|
||||||
"pem",
|
"pem",
|
||||||
|
"proptest",
|
||||||
"prost",
|
"prost",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
@@ -3647,9 +3648,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proptest"
|
name = "proptest"
|
||||||
version = "1.10.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532"
|
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-set",
|
"bit-set",
|
||||||
"bit-vec",
|
"bit-vec",
|
||||||
|
|||||||
@@ -63,5 +63,6 @@ mutants.workspace = true
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = "1.46.3"
|
insta = "1.46.3"
|
||||||
|
proptest = "1.11.0"
|
||||||
rstest.workspace = true
|
rstest.workspace = true
|
||||||
test-log = { version = "0.2", default-features = false, features = ["trace"] }
|
test-log = { version = "0.2", default-features = false, features = ["trace"] }
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use crate::{actors::keyholder, crypto::integrity::hashing::Hashable, safe_cell::SafeCellHandle as _};
|
use crate::{
|
||||||
|
actors::keyholder, crypto::integrity::hashing::Hashable, safe_cell::SafeCellHandle as _,
|
||||||
|
};
|
||||||
use hmac::{Hmac, Mac as _};
|
use hmac::{Hmac, Mac as _};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
|
||||||
@@ -43,7 +45,6 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error("Integrity MAC mismatch for entity {entity_kind}")]
|
#[error("Integrity MAC mismatch for entity {entity_kind}")]
|
||||||
MacMismatch { entity_kind: &'static str },
|
MacMismatch { entity_kind: &'static str },
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -203,13 +204,19 @@ mod tests {
|
|||||||
use diesel::{ExpressionMethods as _, QueryDsl};
|
use diesel::{ExpressionMethods as _, QueryDsl};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use kameo::{actor::ActorRef, prelude::Spawn};
|
use kameo::{actor::ActorRef, prelude::Spawn};
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
|
|
||||||
|
use proptest::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::keyholder::{Bootstrap, KeyHolder}, crypto::integrity::hashing::Hashable, db::{self, schema}, safe_cell::{SafeCell, SafeCellHandle as _}
|
actors::keyholder::{Bootstrap, KeyHolder},
|
||||||
|
db::{self, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Error, Integrable, sign_entity, verify_entity};
|
use super::{Error, Integrable, sign_entity, verify_entity};
|
||||||
|
use super::{hashing::Hashable, payload_hash};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct DummyEntity {
|
struct DummyEntity {
|
||||||
|
|||||||
@@ -104,4 +104,4 @@ impl Hashable for chrono::DateTime<chrono::Utc> {
|
|||||||
fn hash<H: Digest>(&self, hasher: &mut H) {
|
fn hash<H: Digest>(&self, hasher: &mut H) {
|
||||||
hasher.update(&self.timestamp_millis().to_be_bytes());
|
hasher.update(&self.timestamp_millis().to_be_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,8 +84,6 @@ fn shared() -> SharedGrantSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── analyze ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn analyze_matches_empty_calldata() {
|
fn analyze_matches_empty_calldata() {
|
||||||
let m = EtherTransfer::analyze(&ctx(ALLOWED, U256::from(1_000u64))).unwrap();
|
let m = EtherTransfer::analyze(&ctx(ALLOWED, U256::from(1_000u64))).unwrap();
|
||||||
@@ -102,8 +100,6 @@ fn analyze_rejects_nonempty_calldata() {
|
|||||||
assert!(EtherTransfer::analyze(&context).is_none());
|
assert!(EtherTransfer::analyze(&context).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── evaluate ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn evaluate_passes_for_allowed_target() {
|
async fn evaluate_passes_for_allowed_target() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
@@ -276,8 +272,6 @@ async fn evaluate_passes_at_exactly_volume_limit() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── try_find_grant ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn try_find_grant_roundtrip() {
|
async fn try_find_grant_roundtrip() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
@@ -336,7 +330,36 @@ async fn try_find_grant_wrong_target_returns_none() {
|
|||||||
assert!(found.is_none());
|
assert!(found.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── find_all_grants ──────────────────────────────────────────────────────
|
proptest::proptest! {
|
||||||
|
#[test]
|
||||||
|
fn target_order_does_not_affect_hash(
|
||||||
|
raw_addrs in proptest::collection::vec(proptest::prelude::any::<[u8; 20]>(), 0..8),
|
||||||
|
seed in proptest::prelude::any::<u64>(),
|
||||||
|
max_volume in proptest::prelude::any::<u64>(),
|
||||||
|
window_secs in 1i64..=86400,
|
||||||
|
) {
|
||||||
|
use rand::{SeedableRng, seq::SliceRandom};
|
||||||
|
use sha2::Digest;
|
||||||
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
|
let addrs: Vec<Address> = raw_addrs.iter().map(|b| Address::from(*b)).collect();
|
||||||
|
let mut shuffled = addrs.clone();
|
||||||
|
shuffled.shuffle(&mut rand::rngs::StdRng::seed_from_u64(seed));
|
||||||
|
|
||||||
|
let limit = VolumeRateLimit {
|
||||||
|
max_volume: U256::from(max_volume),
|
||||||
|
window: Duration::seconds(window_secs),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut h1 = sha2::Sha256::new();
|
||||||
|
Settings { target: addrs, limit: limit.clone() }.hash(&mut h1);
|
||||||
|
|
||||||
|
let mut h2 = sha2::Sha256::new();
|
||||||
|
Settings { target: shuffled, limit }.hash(&mut h2);
|
||||||
|
|
||||||
|
proptest::prop_assert_eq!(h1.finalize(), h2.finalize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn find_all_grants_empty_db() {
|
async fn find_all_grants_empty_db() {
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use alloy::{
|
|
||||||
primitives::{Address, U256},
|
|
||||||
sol_types::SolCall,
|
|
||||||
};
|
|
||||||
use arbiter_tokens_registry::evm::nonfungible::{self, TokenInfo};
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
|
||||||
use diesel::dsl::{auto_type, insert_into};
|
|
||||||
use diesel::sqlite::Sqlite;
|
|
||||||
use diesel::{ExpressionMethods, prelude::*};
|
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
|
||||||
use crate::db::schema::{
|
use crate::db::schema::{
|
||||||
evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log,
|
evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log,
|
||||||
evm_token_transfer_volume_limit,
|
evm_token_transfer_volume_limit,
|
||||||
@@ -30,6 +20,16 @@ use crate::{
|
|||||||
},
|
},
|
||||||
evm::policies::CombinedSettings,
|
evm::policies::CombinedSettings,
|
||||||
};
|
};
|
||||||
|
use alloy::{
|
||||||
|
primitives::{Address, U256},
|
||||||
|
sol_types::SolCall,
|
||||||
|
};
|
||||||
|
use arbiter_tokens_registry::evm::nonfungible::{self, TokenInfo};
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
use diesel::dsl::{auto_type, insert_into};
|
||||||
|
use diesel::sqlite::Sqlite;
|
||||||
|
use diesel::{ExpressionMethods, prelude::*};
|
||||||
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
|
|
||||||
use super::{DatabaseID, EvalContext, EvalViolation};
|
use super::{DatabaseID, EvalContext, EvalViolation};
|
||||||
|
|
||||||
|
|||||||
@@ -101,8 +101,6 @@ fn shared() -> SharedGrantSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── analyze ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn analyze_known_token_valid_calldata() {
|
fn analyze_known_token_valid_calldata() {
|
||||||
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
|
||||||
@@ -128,8 +126,6 @@ fn analyze_empty_calldata_returns_none() {
|
|||||||
assert!(TokenTransfer::analyze(&ctx(DAI, Bytes::new())).is_none());
|
assert!(TokenTransfer::analyze(&ctx(DAI, Bytes::new())).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── evaluate ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn evaluate_rejects_nonzero_eth_value() {
|
async fn evaluate_rejects_nonzero_eth_value() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
@@ -412,7 +408,39 @@ async fn try_find_grant_unknown_token_returns_none() {
|
|||||||
assert!(found.is_none());
|
assert!(found.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── find_all_grants ──────────────────────────────────────────────────────
|
proptest::proptest! {
|
||||||
|
#[test]
|
||||||
|
fn volume_limits_order_does_not_affect_hash(
|
||||||
|
raw_limits in proptest::collection::vec(
|
||||||
|
(proptest::prelude::any::<u64>(), 1i64..=86400),
|
||||||
|
0..8,
|
||||||
|
),
|
||||||
|
seed in proptest::prelude::any::<u64>(),
|
||||||
|
) {
|
||||||
|
use rand::{SeedableRng, seq::SliceRandom};
|
||||||
|
use sha2::Digest;
|
||||||
|
use crate::crypto::integrity::hashing::Hashable;
|
||||||
|
|
||||||
|
let limits: Vec<VolumeRateLimit> = raw_limits
|
||||||
|
.iter()
|
||||||
|
.map(|(max_vol, window_secs)| VolumeRateLimit {
|
||||||
|
max_volume: U256::from(*max_vol),
|
||||||
|
window: Duration::seconds(*window_secs),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut shuffled = limits.clone();
|
||||||
|
shuffled.shuffle(&mut rand::rngs::StdRng::seed_from_u64(seed));
|
||||||
|
|
||||||
|
let mut h1 = sha2::Sha256::new();
|
||||||
|
Settings { token_contract: DAI, target: None, volume_limits: limits }.hash(&mut h1);
|
||||||
|
|
||||||
|
let mut h2 = sha2::Sha256::new();
|
||||||
|
Settings { token_contract: DAI, target: None, volume_limits: shuffled }.hash(&mut h2);
|
||||||
|
|
||||||
|
proptest::prop_assert_eq!(h1.finalize(), h2.finalize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn find_all_grants_empty_db() {
|
async fn find_all_grants_empty_db() {
|
||||||
|
|||||||
Reference in New Issue
Block a user