Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions bin/txload/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ struct Args {
#[arg(long, default_value = "http://127.0.0.1:8545")]
rpc_url: String,

/// Chain ID used for signing EIP-1559 transactions
#[arg(long, default_value_t = 1)]
/// Chain ID used for signing EIP-1559 transactions (default matches Evolve's DEFAULT_CHAIN_ID)
#[arg(long, default_value_t = 900_901)]
chain_id: u64,

/// Token account Ethereum address (0x...) that exposes `transfer`
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);
}
}
}
});

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