Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
3 changes: 2 additions & 1 deletion bin/testapp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,8 @@ mod tests {
token_account_id: AccountId,
account_id: AccountId,
) -> u128 {
let mut key = token_account_id.as_bytes().to_vec();
let mut key = vec![evolve_core::runtime_api::ACCOUNT_STORAGE_PREFIX];
key.extend_from_slice(&token_account_id.as_bytes());
key.push(1u8);
key.extend(account_id.encode().expect("encode account id"));

Expand Down
3 changes: 2 additions & 1 deletion bin/testapp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,8 @@ mod tests {
token_account_id: AccountId,
account_id: AccountId,
) -> u128 {
let mut key = token_account_id.as_bytes().to_vec();
let mut key = vec![evolve_core::runtime_api::ACCOUNT_STORAGE_PREFIX];
key.extend_from_slice(&token_account_id.as_bytes());
key.push(1u8); // Token::balances storage prefix
key.extend(account_id.encode().expect("encode account id"));

Expand Down
29 changes: 20 additions & 9 deletions bin/testapp/tests/mempool_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,18 +275,21 @@ impl AsyncMockStorage {

/// Initialize an EthEoaAccount's storage (nonce and eth_address).
fn init_eth_eoa_storage(&self, account_id: AccountId, eth_address: [u8; 20]) {
// Storage keys are: account_id + prefix (u8)
use evolve_core::runtime_api::ACCOUNT_STORAGE_PREFIX;
// Storage keys are: ACCOUNT_STORAGE_PREFIX + account_id + prefix (u8)
// Item::new(0) = nonce, Item::new(1) = eth_address
let mut data = self.data.write().unwrap();

let mut nonce_key = account_id.as_bytes().to_vec();
let mut nonce_key = vec![ACCOUNT_STORAGE_PREFIX];
nonce_key.extend_from_slice(&account_id.as_bytes());
nonce_key.push(0u8);
data.insert(
nonce_key,
Message::new(&0u64).unwrap().into_bytes().unwrap(),
);

let mut addr_key = account_id.as_bytes().to_vec();
let mut addr_key = vec![ACCOUNT_STORAGE_PREFIX];
addr_key.extend_from_slice(&account_id.as_bytes());
addr_key.push(1u8);
data.insert(
addr_key,
Expand All @@ -296,18 +299,21 @@ impl AsyncMockStorage {

/// Initialize an Ed25519AuthAccount's storage (nonce and public key).
fn init_ed25519_auth_storage(&self, account_id: AccountId, public_key: [u8; 32]) {
// Storage keys are: account_id + prefix (u8)
use evolve_core::runtime_api::ACCOUNT_STORAGE_PREFIX;
// Storage keys are: ACCOUNT_STORAGE_PREFIX + account_id + prefix (u8)
// Item::new(0) = nonce, Item::new(1) = public key
let mut data = self.data.write().unwrap();

let mut nonce_key = account_id.as_bytes().to_vec();
let mut nonce_key = vec![ACCOUNT_STORAGE_PREFIX];
nonce_key.extend_from_slice(&account_id.as_bytes());
nonce_key.push(0u8);
data.insert(
nonce_key,
Message::new(&0u64).unwrap().into_bytes().unwrap(),
);

let mut pubkey_key = account_id.as_bytes().to_vec();
let mut pubkey_key = vec![ACCOUNT_STORAGE_PREFIX];
pubkey_key.extend_from_slice(&account_id.as_bytes());
pubkey_key.push(1u8);
data.insert(
pubkey_key,
Expand All @@ -317,7 +323,9 @@ impl AsyncMockStorage {

/// Set token balance directly in storage for a specific account.
fn set_token_balance(&self, token_account_id: AccountId, account_id: AccountId, balance: u128) {
let mut key = token_account_id.as_bytes().to_vec();
use evolve_core::runtime_api::ACCOUNT_STORAGE_PREFIX;
let mut key = vec![ACCOUNT_STORAGE_PREFIX];
key.extend_from_slice(&token_account_id.as_bytes());
key.push(1u8); // Token::balances storage prefix
key.extend(account_id.encode().expect("encode account id"));
let value = Message::new(&balance).unwrap().into_bytes().unwrap();
Expand Down Expand Up @@ -417,9 +425,11 @@ fn create_signed_tx(
}

fn read_nonce<S: ReadonlyKV>(storage: &S, account_id: AccountId) -> u64 {
use evolve_core::runtime_api::ACCOUNT_STORAGE_PREFIX;
use evolve_core::Message;

let mut nonce_key = account_id.as_bytes().to_vec();
let mut nonce_key = vec![ACCOUNT_STORAGE_PREFIX];
nonce_key.extend_from_slice(&account_id.as_bytes());
nonce_key.push(0u8);
match storage.get(&nonce_key).expect("read nonce") {
Some(value) => Message::from_bytes(value)
Expand All @@ -437,7 +447,8 @@ fn read_token_balance<S: ReadonlyKV>(
use evolve_core::encoding::Encodable;
use evolve_core::Message;

let mut key = token_account_id.as_bytes().to_vec();
let mut key = vec![evolve_core::runtime_api::ACCOUNT_STORAGE_PREFIX];
key.extend_from_slice(&token_account_id.as_bytes());
key.push(1u8); // Token::balances storage prefix
key.extend(account_id.encode().expect("encode account id"));

Expand Down
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
Loading
Loading