Skip to content

Commit 8eab316

Browse files
authored
fix: track last config update ledger sequence for backend cache invalidation (#22)
Wires the existing config_metadata::record_config_update() helper into the success path of set_config(), immediately after the storage write and before the cfg_upd event publish. Adds a new contract-level getter get_last_config_update() that exposes the recorded ledger sequence so backends can use it as a cheap cache-invalidation signal: compare the returned sequence against the one observed at the last get_config_snapshot() and refetch only when it has advanced, eliminating eager re-polls. Implementation notes: - The sequence is wrapped in a #[contracttype] ConfigUpdateInfo { sequence: u32 } struct so that Option<ConfigUpdateInfo> survives the Soroban-generated client (bare Option<u32> is empirically flattened to u32 for primitives). - LAST_CFG_UPDATE_KEY is now re-exported at the crate root and added to the storage-key namespace regression test (test_storage_key_namespace_symbols_are_distinct). - The internal record/get_last_config_update helpers now access instance storage inside env.as_contract(...) so they work in the unit-test context. Tests: - Four new acceptance tests appended covering: None before init, Some after set_config, recorded_sequence matches ledger, and strictly increasing sequences on repeated updates. - cargo fmt --check clean. - cargo build passes. - Full library test suite (380 tests) passes.
1 parent 5a4d8a2 commit 8eab316

3 files changed

Lines changed: 153 additions & 8 deletions

File tree

apexchainx_calculator/src/config_metadata.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@ use soroban_sdk::{symbol_short, Env, Symbol};
1515

1616
/// On-chain key storing the ledger sequence of the last config update.
1717
/// "LCFGUPD" = Last ConFiG UPDate.
18-
const LAST_CFG_UPDATE_KEY: Symbol = symbol_short!("LCFGUPD");
18+
pub const LAST_CFG_UPDATE_KEY: Symbol = symbol_short!("LCFGUPD");
1919

2020
/// Records the current ledger sequence as the time of the latest config update.
2121
/// Called internally by `set_config` after a successful update.
2222
pub fn record_config_update(env: &Env) {
2323
let ledger = env.ledger().sequence();
24-
env.storage()
25-
.instance()
26-
.set(&LAST_CFG_UPDATE_KEY, &ledger);
24+
env.storage().instance().set(&LAST_CFG_UPDATE_KEY, &ledger);
2725
}
2826

2927
/// Returns the ledger sequence of the last configuration update.
@@ -35,19 +33,29 @@ pub fn get_last_config_update(env: &Env) -> Option<u32> {
3533
#[cfg(test)]
3634
mod tests {
3735
use super::*;
36+
use crate::SLACalculatorContract;
3837
use soroban_sdk::Env;
3938

39+
// These tests exercise the helper functions in isolation through the
40+
// contract's instance-storage context. Without `env.as_contract(...)`,
41+
// Soroban rejects instance-storage access from a bare `Env::default()`.
4042
#[test]
4143
fn test_last_config_update_unset() {
4244
let env = Env::default();
43-
assert_eq!(get_last_config_update(&env), None);
45+
let contract_id = env.register_contract(None, SLACalculatorContract);
46+
env.as_contract(&contract_id, || {
47+
assert_eq!(get_last_config_update(&env), None);
48+
});
4449
}
4550

4651
#[test]
4752
fn test_record_and_read_config_update() {
4853
let env = Env::default();
49-
record_config_update(&env);
50-
let ledger = get_last_config_update(&env);
51-
assert!(ledger.is_some());
54+
let contract_id = env.register_contract(None, SLACalculatorContract);
55+
env.as_contract(&contract_id, || {
56+
record_config_update(&env);
57+
let ledger = get_last_config_update(&env);
58+
assert!(ledger.is_some());
59+
});
5260
}
5361
}

apexchainx_calculator/src/lib.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub struct SLACalculatorContract;
1212
#[cfg(test)]
1313
mod tests;
1414

15+
pub mod config_metadata;
1516
pub mod coordination_harness;
1617
pub mod cross_contract_safety;
1718
pub mod event_correlation;
@@ -76,6 +77,10 @@ const MAX_HISTORY_SIZE: u32 = 1000;
7677
/// When set, overrides MAX_HISTORY_SIZE for history trimming.
7778
const RETENTION_LIMIT_KEY: Symbol = symbol_short!("RETLIM");
7879

80+
/// On-chain key storing the ledger sequence of the last config update. Re-exported
81+
/// here so the storage-key namespace regression test catches any future collisions.
82+
pub use crate::config_metadata::LAST_CFG_UPDATE_KEY;
83+
7984
// -----------------------------------------------------------------------
8085
// Event Constants
8186
// -----------------------------------------------------------------------
@@ -372,6 +377,19 @@ pub struct PauseInfo {
372377
pub paused_by: Address,
373378
}
374379

380+
/// #4 – Metadata about the most recent configuration update.
381+
///
382+
/// Wrapping the ledger sequence in a contract type (rather than exposing it
383+
/// directly as `Option<u32>`) preserves the `Some`/`None` distinction when
384+
/// the value crosses the Soroban contract client boundary — primitives
385+
/// wrapped in `Option` are otherwise flattened and lose the null case.
386+
#[contracttype]
387+
#[derive(Clone, Debug, Eq, PartialEq)]
388+
pub struct ConfigUpdateInfo {
389+
/// Ledger sequence at which the most recent `set_config` succeeded.
390+
pub sequence: u32,
391+
}
392+
375393
/// SC-021 – Storage version and migration posture for off-chain consumers.
376394
///
377395
/// Backend consumers should call `get_migration_state` after any contract upgrade
@@ -840,6 +858,12 @@ impl SLACalculatorContract {
840858
);
841859
env.storage().instance().set(&CONFIG_KEY, &configs);
842860

861+
// Issue #4 – stamp the ledger sequence of the most recent config
862+
// update so backends can detect when their cached configuration is
863+
// stale. Called after the storage write so the recorded sequence
864+
// always reflects a successful update.
865+
config_metadata::record_config_update(&env);
866+
843867
env.events().publish(
844868
(EVENT_CONFIG_UPD, EVENT_VERSION, severity),
845869
(threshold_minutes, penalty_per_minute, reward_base),
@@ -860,6 +884,25 @@ impl SLACalculatorContract {
860884
.ok_or(SLAError::NotInitialized)
861885
}
862886

887+
/// #4 – Returns metadata about the most recent configuration update,
888+
/// or `None` if no `set_config` call has been recorded since
889+
/// initialization.
890+
///
891+
/// Backend consumers compare `update.sequence` against the ledger
892+
/// sequence they observed at their last `get_config_snapshot()` to
893+
/// decide whether their cached configuration is stale and needs to be
894+
/// re-fetched. This enables cheap cache invalidation without polling the
895+
/// full configuration on every health check.
896+
///
897+
/// The result is wrapped in `Option<ConfigUpdateInfo>` (rather than
898+
/// `Option<u32>`) so the `Some`/`None` distinction survives the
899+
/// Soroban contract client boundary.
900+
pub fn get_last_config_update(env: Env) -> Result<Option<ConfigUpdateInfo>, SLAError> {
901+
Self::check_version(&env)?;
902+
Ok(config_metadata::get_last_config_update(&env)
903+
.map(|seq| ConfigUpdateInfo { sequence: seq }))
904+
}
905+
863906
/// Returns a deterministic backend-friendly snapshot of all config values.
864907
pub fn get_config_snapshot(env: Env) -> Result<SLAConfigSnapshot, SLAError> {
865908
Self::check_version(&env)?;

apexchainx_calculator/src/tests.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6131,3 +6131,97 @@ fn test_257_hash_differs_across_all_four_severities() {
61316131
let h4 = client.get_config_version_hash();
61326132
assert_ne!(h3, h4);
61336133
}
6134+
6135+
// ============================================================
6136+
// Issue #4 – Config update metadata tracking
6137+
// ============================================================
6138+
//
6139+
// These tests cover (1) declaring the existing `config_metadata` module,
6140+
// (2) wiring `record_config_update()` into `set_config()` after the
6141+
// successful storage write, and (3) exposing the recorded ledger sequence
6142+
// through `get_last_config_update()`. Backends use the returned sequence as
6143+
// a cheap cache-invalidation signal: compare it against the ledger sequence
6144+
// observed at the last `get_config_snapshot()` and re-fetch only when it has
6145+
// advanced.
6146+
6147+
/// Acceptance criterion (a): after `initialize()` but before any
6148+
/// `set_config` call, no update has been recorded – the getter must
6149+
/// return `None`.
6150+
#[test]
6151+
fn test_issue4_get_last_config_update_is_none_after_initialize() {
6152+
let (_env, client, _actors) = setup();
6153+
assert_eq!(client.get_last_config_update(), None);
6154+
}
6155+
6156+
/// Acceptance criterion (b): once `set_config` succeeds, the getter must
6157+
/// return `Some(ConfigUpdateInfo)`.
6158+
#[test]
6159+
fn test_issue4_get_last_config_update_is_some_after_set_config() {
6160+
let (_env, client, actors) = setup();
6161+
client.set_config(&actors.admin, &symbol_short!("critical"), &20, &200, &1000);
6162+
let recorded = client.get_last_config_update();
6163+
assert!(
6164+
recorded.is_some(),
6165+
"get_last_config_update must be Some(_) after set_config"
6166+
);
6167+
}
6168+
6169+
/// Acceptance criterion (c): the recorded sequence must equal the
6170+
/// ledger sequence observed at the moment of the `set_config` call.
6171+
#[test]
6172+
fn test_issue4_get_last_config_update_matches_ledger_sequence() {
6173+
let (env, client, actors) = setup();
6174+
let sequence_before = env.ledger().sequence();
6175+
6176+
client.set_config(&actors.admin, &symbol_short!("critical"), &20, &200, &1000);
6177+
6178+
let recorded = client.get_last_config_update().unwrap();
6179+
// Within a single test ledger the sequence never advances on its own,
6180+
// so any read during this test must match the recorded one.
6181+
assert_eq!(
6182+
recorded.sequence, sequence_before,
6183+
"recorded sequence must match the ledger sequence at update time"
6184+
);
6185+
assert_eq!(
6186+
recorded.sequence,
6187+
env.ledger().sequence(),
6188+
"recorded sequence must match the current ledger sequence within the same test ledger"
6189+
);
6190+
}
6191+
6192+
/// Acceptance criterion (d): repeated `set_config` calls performed at
6193+
/// strictly increasing ledger sequences must produce strictly increasing
6194+
/// recorded sequences.
6195+
#[test]
6196+
fn test_issue4_repeated_set_config_produces_increasing_sequences() {
6197+
let env = Env::default();
6198+
6199+
let cid = env.register_contract(None, SLACalculatorContract);
6200+
let client = SLACalculatorContractClient::new(&env, &cid);
6201+
let admin = soroban_sdk::Address::generate(&env);
6202+
let op = soroban_sdk::Address::generate(&env);
6203+
client.initialize(&admin, &op);
6204+
6205+
// Advance the ledger to a known starting sequence and trigger an update.
6206+
env.ledger().with_mut(|li| {
6207+
li.sequence_number = 100;
6208+
});
6209+
client.set_config(&admin, &symbol_short!("critical"), &20, &200, &1000);
6210+
let first = client.get_last_config_update().unwrap();
6211+
assert_eq!(first.sequence, 100);
6212+
6213+
// Advance further and trigger another update on a different severity.
6214+
env.ledger().with_mut(|li| {
6215+
li.sequence_number = 250;
6216+
});
6217+
client.set_config(&admin, &symbol_short!("high"), &45, &75, &800);
6218+
let second = client.get_last_config_update().unwrap();
6219+
assert_eq!(second.sequence, 250);
6220+
6221+
assert!(
6222+
second.sequence > first.sequence,
6223+
"Repeated updates must produce an increasing sequence: second={} first={}",
6224+
second.sequence,
6225+
first.sequence
6226+
);
6227+
}

0 commit comments

Comments
 (0)