Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions crates/app/node/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ pub struct ChainConfig {

impl Default for ChainConfig {
fn default() -> Self {
Self { chain_id: 1 }
// 900_901 is deliberately not a live EVM network to prevent cross-chain replay.
Self { chain_id: 900_901 }
}
}

Expand Down Expand Up @@ -227,7 +228,7 @@ mod tests {
.extract()
.expect("figment extract failed");

assert_eq!(loaded.chain.chain_id, 1);
assert_eq!(loaded.chain.chain_id, 900_901);
assert_eq!(loaded.storage.path, DEFAULT_DATA_DIR);
assert_eq!(loaded.rpc.http_addr, DEFAULT_RPC_ADDR);
assert_eq!(loaded.grpc.addr, DEFAULT_GRPC_ADDR);
Expand Down
12 changes: 11 additions & 1 deletion crates/app/node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ type RuntimeContext = TokioContext;
/// subsystem — all produced blocks must be persisted.
async fn build_block_archive(context: TokioContext) -> OnBlockArchive {
let config = BlockStorageConfig::default();
let retention = config.retention_blocks;
let prune_interval = config.blocks_per_section;
let store = BlockStorage::new(context, config)
.await
.expect("failed to initialize block archive storage");
Expand All @@ -236,10 +238,18 @@ async fn build_block_archive(context: TokioContext) -> OnBlockArchive {
if let Err(e) = store.put_sync(block_number, block_hash, block_bytes).await {
tracing::warn!("Failed to archive block {}: {:?}", block_number, e);
}

// Prune old blocks at section boundaries to bound disk usage.
if retention > 0 && block_number > retention && block_number % prune_interval == 0 {
let min_block = block_number.saturating_sub(retention);
if let Err(e) = store.prune(min_block).await {
tracing::warn!(min_block, "Failed to prune block archive: {:?}", e);
}
}
Comment thread
tac0turtle marked this conversation as resolved.
}
});

tracing::info!("Block archive storage enabled");
tracing::info!(retention, "Block archive storage enabled");

Arc::new(move |block_number, block_hash, block_bytes| {
let hash_bytes = ArchiveBlockHash::new(block_hash.0);
Expand Down
1 change: 1 addition & 0 deletions crates/app/sdk/collections/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ error-decode = ["linkme", "evolve_core/error-decode"]

[dev-dependencies]
proptest = "1.4"
evolve_testing = { workspace = true, features = ["proptest"] }
102 changes: 76 additions & 26 deletions crates/app/sdk/collections/src/prop_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,13 @@ use crate::queue::Queue;
use crate::unordered_map::UnorderedMap;
use crate::vector::Vector;
use crate::ERR_EMPTY;
use crate::ERR_NOT_FOUND;
use evolve_testing::proptest_config::proptest_config;
use proptest::prelude::*;
use std::collections::{HashMap, VecDeque};
use std::collections::{BTreeMap, HashMap, VecDeque};

const MAX_OPS: usize = 32;
const MAX_KEYS: usize = 16;
const DEFAULT_CASES: u32 = 128;
const CI_CASES: u32 = 32;

fn proptest_cases() -> u32 {
if let Ok(value) = std::env::var("EVOLVE_PROPTEST_CASES") {
if let Ok(parsed) = value.parse::<u32>() {
if parsed > 0 {
return parsed;
}
}
}

if std::env::var("EVOLVE_CI").is_ok() || std::env::var("CI").is_ok() {
return CI_CASES;
}

DEFAULT_CASES
}

fn proptest_config() -> proptest::test_runner::Config {
proptest::test_runner::Config {
cases: proptest_cases(),
..Default::default()
}
}

proptest! {
#![proptest_config(proptest_config())]
Expand Down Expand Up @@ -216,3 +193,76 @@ proptest! {
prop_assert_eq!(actual_pairs, expected_pairs);
}
}

// ============================================================================
// Map model-based test
// ============================================================================

#[derive(Clone, Debug)]
enum MapOp {
Set { key: u64, value: u64 },
Get { key: u64 },
Remove { key: u64 },
Exists { key: u64 },
}

fn map_ops_strategy() -> impl Strategy<Value = Vec<MapOp>> {
let keys: Vec<u64> = (0..MAX_KEYS as u64).collect();

let set = (proptest::sample::select(keys.clone()), any::<u64>())
.prop_map(|(key, value)| MapOp::Set { key, value });
let get = proptest::sample::select(keys.clone()).prop_map(|key| MapOp::Get { key });
let remove = proptest::sample::select(keys.clone()).prop_map(|key| MapOp::Remove { key });
let exists = proptest::sample::select(keys).prop_map(|key| MapOp::Exists { key });

let op = prop_oneof![4 => set, 2 => get, 2 => remove, 1 => exists];
proptest::collection::vec(op, 0..=MAX_OPS)
}

proptest! {
#![proptest_config(proptest_config())]

#[test]
fn prop_map_matches_model(ops in map_ops_strategy()) {
let map: Map<u64, u64> = Map::new(50);
let mut env = MockEnvironment::new(1, 2);
let mut model: BTreeMap<u64, u64> = BTreeMap::new();

for op in ops {
match op {
MapOp::Set { key, value } => {
map.set(&key, &value, &mut env).unwrap();
model.insert(key, value);
}
MapOp::Get { key } => {
let actual = map.may_get(&key, &mut env).unwrap();
let expected = model.get(&key).copied();
prop_assert_eq!(actual, expected);

// Also verify get() returns ERR_NOT_FOUND for missing keys
if expected.is_none() {
prop_assert_eq!(map.get(&key, &mut env).unwrap_err(), ERR_NOT_FOUND);
} else {
prop_assert_eq!(map.get(&key, &mut env).unwrap(), expected.unwrap());
}
}
MapOp::Remove { key } => {
map.remove(&key, &mut env).unwrap();
model.remove(&key);
}
MapOp::Exists { key } => {
let actual = map.exists(&key, &mut env).unwrap();
let expected = model.contains_key(&key);
prop_assert_eq!(actual, expected);
}
}
}

// Final state: verify all keys match the model
for key in 0..MAX_KEYS as u64 {
let expected = model.get(&key).copied();
let actual = map.may_get(&key, &mut env).unwrap();
prop_assert_eq!(actual, expected);
}
}
}
1 change: 1 addition & 0 deletions crates/app/sdk/core/src/runtime_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::{AccountId, InvokableMessage, InvokeRequest, InvokeResponse};
use borsh::{BorshDeserialize, BorshSerialize};
pub const ACCOUNT_IDENTIFIER_PREFIX: u8 = 0;
pub const ACCOUNT_IDENTIFIER_SINGLETON_PREFIX: u8 = 1;
pub const ACCOUNT_STORAGE_PREFIX: u8 = 2;
pub const RUNTIME_ACCOUNT_ID: AccountId = AccountId::from_u64(0);

/// Storage key for consensus parameters.
Expand Down
5 changes: 5 additions & 0 deletions crates/app/sdk/testing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ rust-version.workspace = true
[dependencies]
evolve_core.workspace = true
evolve_stf_traits.workspace = true
proptest = { version = "1.4", optional = true }

[features]
default = []
proptest = ["dep:proptest"]

[lints]
workspace = true
13 changes: 10 additions & 3 deletions crates/app/sdk/testing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
// Testing code - determinism requirements do not apply.
#![allow(clippy::disallowed_types)]

#[cfg(feature = "proptest")]
pub mod proptest_config;

pub mod server_mocks;

use evolve_core::encoding::{Decodable, Encodable};
use evolve_core::runtime_api::ACCOUNT_STORAGE_PREFIX;
use evolve_core::storage_api::{
StorageGetRequest, StorageGetResponse, StorageRemoveRequest, StorageRemoveResponse,
StorageSetRequest, StorageSetResponse, STORAGE_ACCOUNT_ID,
Expand Down Expand Up @@ -111,7 +115,8 @@ impl MockEnv {
StorageSetRequest::FUNCTION_IDENTIFIER => {
let storage_set: StorageSetRequest = request.get()?;

let mut key = self.whoami.as_bytes().to_vec();
let mut key = vec![ACCOUNT_STORAGE_PREFIX];
key.extend_from_slice(&self.whoami.as_bytes());
key.extend(storage_set.key);

self.state.insert(key, storage_set.value.as_vec()?);
Expand All @@ -120,7 +125,8 @@ impl MockEnv {
}
StorageRemoveRequest::FUNCTION_IDENTIFIER => {
let storage_remove: StorageRemoveRequest = request.get()?;
let mut key = self.whoami.as_bytes().to_vec();
let mut key = vec![ACCOUNT_STORAGE_PREFIX];
key.extend_from_slice(&self.whoami.as_bytes());
key.extend(storage_remove.key);
self.state.remove(&key);
Ok(InvokeResponse::new(&StorageRemoveResponse {})?)
Expand All @@ -134,7 +140,8 @@ impl MockEnv {
StorageGetRequest::FUNCTION_IDENTIFIER => {
let storage_get: StorageGetRequest = request.get()?;

let mut key = storage_get.account_id.as_bytes().to_vec();
let mut key = vec![ACCOUNT_STORAGE_PREFIX];
key.extend_from_slice(&storage_get.account_id.as_bytes());
key.extend(storage_get.key);

let value = self.state.get(&key).cloned();
Expand Down
38 changes: 38 additions & 0 deletions crates/app/sdk/testing/src/proptest_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! Shared property-test configuration for the Evolve workspace.
//!
//! Provides a single source of truth for case counts so every crate
//! respects `EVOLVE_PROPTEST_CASES`, CI detection, and a sensible local
//! default without duplicating the logic.

const DEFAULT_CASES: u32 = 128;
const CI_CASES: u32 = 32;

/// Return the number of proptest cases to run.
///
/// Priority:
/// 1. `EVOLVE_PROPTEST_CASES` env var (must parse to a positive `u32`).
/// 2. `CI` or `EVOLVE_CI` env var present → [`CI_CASES`].
/// 3. Otherwise → [`DEFAULT_CASES`].
pub fn proptest_cases() -> u32 {
if let Ok(value) = std::env::var("EVOLVE_PROPTEST_CASES") {
if let Ok(parsed) = value.parse::<u32>() {
if parsed > 0 {
return parsed;
}
}
}

if std::env::var("EVOLVE_CI").is_ok() || std::env::var("CI").is_ok() {
return CI_CASES;
}

DEFAULT_CASES
}

/// Build a [`proptest::test_runner::Config`] using [`proptest_cases`].
pub fn proptest_config() -> proptest::test_runner::Config {
proptest::test_runner::Config {
cases: proptest_cases(),
..Default::default()
}
}
1 change: 1 addition & 0 deletions crates/app/stf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ linkme = {version = "0.3", default-features = false, optional = true}

[dev-dependencies]
proptest = "1.4"
evolve_testing = { workspace = true, features = ["proptest"] }

[lints]
workspace = true
Expand Down
26 changes: 1 addition & 25 deletions crates/app/stf/src/execution_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,36 +558,12 @@ mod tests {
use super::*;
// bring in the Checkpoint and StateChange
use evolve_core::{ErrorCode, Message, ReadonlyKV};
use evolve_testing::proptest_config::proptest_config;
use proptest::prelude::*;
use std::collections::HashMap;

const MAX_OPS: usize = 64;
const MAX_KEYS: u8 = 16;
const DEFAULT_CASES: u32 = 128;
const CI_CASES: u32 = 32;

fn proptest_cases() -> u32 {
if let Ok(value) = std::env::var("EVOLVE_PROPTEST_CASES") {
if let Ok(parsed) = value.parse::<u32>() {
if parsed > 0 {
return parsed;
}
}
}

if std::env::var("EVOLVE_CI").is_ok() || std::env::var("CI").is_ok() {
return CI_CASES;
}

DEFAULT_CASES
}

fn proptest_config() -> proptest::test_runner::Config {
proptest::test_runner::Config {
cases: proptest_cases(),
..Default::default()
}
}

#[derive(Clone, Debug)]
enum Op {
Expand Down
26 changes: 1 addition & 25 deletions crates/app/stf/src/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,34 +279,10 @@ impl GasCounter {
mod tests {
use super::*;
use evolve_core::Message;
use evolve_testing::proptest_config::proptest_config;
use proptest::prelude::*;

const MAX_OPS: usize = 64;
const DEFAULT_CASES: u32 = 128;
const CI_CASES: u32 = 32;

fn proptest_cases() -> u32 {
if let Ok(value) = std::env::var("EVOLVE_PROPTEST_CASES") {
if let Ok(parsed) = value.parse::<u32>() {
if parsed > 0 {
return parsed;
}
}
}

if std::env::var("EVOLVE_CI").is_ok() || std::env::var("CI").is_ok() {
return CI_CASES;
}

DEFAULT_CASES
}

fn proptest_config() -> proptest::test_runner::Config {
proptest::test_runner::Config {
cases: proptest_cases(),
..Default::default()
}
}

#[derive(Clone, Debug)]
enum Op {
Expand Down
11 changes: 7 additions & 4 deletions crates/app/stf/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::invoker::Invoker;
use crate::runtime_api_impl;
use evolve_core::runtime_api::{
CreateAccountRequest, CreateAccountResponse, MigrateRequest, RegisterAccountAtIdRequest,
RegisterAccountAtIdResponse, RUNTIME_ACCOUNT_ID,
RegisterAccountAtIdResponse, ACCOUNT_STORAGE_PREFIX, RUNTIME_ACCOUNT_ID,
};
use evolve_core::storage_api::{
StorageGetRequest, StorageGetResponse, StorageRemoveRequest, StorageRemoveResponse,
Expand Down Expand Up @@ -120,7 +120,8 @@ pub fn handle_storage_exec<S: ReadonlyKV, A: AccountsCodeStorage>(
StorageSetRequest::FUNCTION_IDENTIFIER => {
let storage_set: StorageSetRequest = request.get()?;

let mut key = invoker.whoami.as_bytes().to_vec();
let mut key = vec![ACCOUNT_STORAGE_PREFIX];
key.extend_from_slice(&invoker.whoami.as_bytes());
key.extend(storage_set.key);

// increase gas costs
Expand All @@ -133,7 +134,8 @@ pub fn handle_storage_exec<S: ReadonlyKV, A: AccountsCodeStorage>(
}
StorageRemoveRequest::FUNCTION_IDENTIFIER => {
let storage_remove: StorageRemoveRequest = request.get()?;
let mut key = invoker.whoami.as_bytes().to_vec();
let mut key = vec![ACCOUNT_STORAGE_PREFIX];
key.extend_from_slice(&invoker.whoami.as_bytes());
key.extend(storage_remove.key);
invoker.gas_counter.consume_remove_gas(&key)?;
invoker.storage.remove(&key)?;
Expand All @@ -151,7 +153,8 @@ pub fn handle_storage_query<S: ReadonlyKV, A: AccountsCodeStorage>(
StorageGetRequest::FUNCTION_IDENTIFIER => {
let storage_get: StorageGetRequest = request.get()?;

let mut key = storage_get.account_id.as_bytes().to_vec();
let mut key = vec![ACCOUNT_STORAGE_PREFIX];
key.extend_from_slice(&storage_get.account_id.as_bytes());
key.extend(storage_get.key);

let value = invoker.storage.get(&key)?;
Expand Down
Loading
Loading