diff --git a/apexchainx_calculator/src/config.rs b/apexchainx_calculator/src/config.rs index 58503b0..a00e7cd 100644 --- a/apexchainx_calculator/src/config.rs +++ b/apexchainx_calculator/src/config.rs @@ -8,7 +8,7 @@ use soroban_sdk::{symbol_short, Env, Map, Symbol, Vec}; use crate::{ config_freeze, config_metadata, SLAConfig, SLAConfigEntry, SLAConfigSnapshot, SLAError, CONFIG_KEY, - CUSTOM_CONFIG_KEY, EVENT_CONFIG_UPD, EVENT_VERSION, + CUSTOM_CONFIG_KEY, EVENT_CONFIG_REM, EVENT_CONFIG_UPD, EVENT_SEV_ADD, EVENT_SEV_UPD, EVENT_VERSION, }; /// Sets the SLA configuration for a given severity level. @@ -104,10 +104,15 @@ pub fn get_last_config_update(env: &Env) -> Result Result<(), SLAErro env.storage().instance().set(&CUSTOM_CONFIG_KEY, &custom); env.events() - .publish((EVENT_CONFIG_UPD, EVENT_VERSION, severity), (0u32, 0i128, 0i128)); + .publish((EVENT_CONFIG_REM, EVENT_VERSION, severity), ()); Ok(()) } diff --git a/apexchainx_calculator/src/event_schema.rs b/apexchainx_calculator/src/event_schema.rs index 6378d9b..c78e701 100644 --- a/apexchainx_calculator/src/event_schema.rs +++ b/apexchainx_calculator/src/event_schema.rs @@ -44,6 +44,23 @@ //! - repeated writes preserve invocation order; see the regression policy in //! `docs/PROJECT_CONTEXT.md` //! +//! ## sev_add (`sev_add`) +//! Emitted when `set_custom_severity` registers a **new** custom severity. +//! - topic[2]: custom severity Symbol +//! - payload: (threshold_minutes: u32, penalty_per_minute: i128, +//! reward_base: i128) +//! +//! ## sev_upd (`sev_upd`) +//! Emitted when `set_custom_severity` **reconfigures** an existing one. +//! - topic[2]: custom severity Symbol +//! - payload: (threshold_minutes: u32, penalty_per_minute: i128, +//! reward_base: i128) +//! +//! ## cfg_rem (`cfg_rem`) +//! Emitted when `remove_custom_severity` deletes a custom severity. +//! - topic[2]: custom severity Symbol +//! - payload: () +//! //! ## paused (`paused`) //! Emitted when the contract is paused. //! - topic[2]: caller Address @@ -195,6 +212,14 @@ pub const EVENT_SETTLE_INTENT: Symbol = symbol_short!("set_int"); pub const EVENT_CONFIG_UPD: Symbol = symbol_short!("cfg_upd"); /// Emitted when a custom severity is removed via remove_custom_severity. pub const EVENT_CONFIG_REM: Symbol = symbol_short!("cfg_rem"); +/// Emitted when a new custom severity is registered (first creation). +/// Distinguishable from cfg_upd by indexers: the custom severity did not +/// exist before this call. (#456) +pub const EVENT_SEV_ADD: Symbol = symbol_short!("sev_add"); +/// Emitted when an existing custom severity is reconfigured. +/// Distinguishable from sev_add by indexers: the custom severity already +/// existed before this call. (#456) +pub const EVENT_SEV_UPD: Symbol = symbol_short!("sev_upd"); pub const EVENT_PAUSED: Symbol = symbol_short!("paused"); pub const EVENT_UNPAUSED: Symbol = symbol_short!("unpause"); pub const EVENT_OP_SET: Symbol = symbol_short!("op_set"); @@ -237,6 +262,8 @@ mod tests { EVENT_SETTLE_INTENT, EVENT_CONFIG_UPD, EVENT_CONFIG_REM, + EVENT_SEV_ADD, + EVENT_SEV_UPD, EVENT_PAUSED, EVENT_UNPAUSED, EVENT_OP_SET, diff --git a/apexchainx_calculator/src/lib.rs b/apexchainx_calculator/src/lib.rs index fdc6b1b..f56092f 100644 --- a/apexchainx_calculator/src/lib.rs +++ b/apexchainx_calculator/src/lib.rs @@ -325,6 +325,23 @@ pub(crate) const EVENT_CONFIG_UPD: Symbol = symbol_short!("cfg_upd"); /// other removal-style events. The removed severity is carried in topic[2]. pub(crate) const EVENT_CONFIG_REM: Symbol = symbol_short!("cfg_rem"); +/// Emitted when a new custom severity is registered (first creation). +/// Distinguishable from cfg_upd by indexers: the custom severity did not +/// exist before this call. (#456) +/// +/// Compatibility decision: payload is `(threshold_minutes, penalty_per_minute, +/// reward_base)` — same shape as cfg_upd. The distinct event name lets +/// indexers separate creation from update without state inspection. +pub(crate) const EVENT_SEV_ADD: Symbol = symbol_short!("sev_add"); + +/// Emitted when an existing custom severity is reconfigured. +/// Distinguishable from sev_add by indexers: the custom severity already +/// existed before this call. (#456) +/// +/// Compatibility decision: payload is `(threshold_minutes, penalty_per_minute, +/// reward_base)` — same shape as cfg_upd. +pub(crate) const EVENT_SEV_UPD: Symbol = symbol_short!("sev_upd"); + /// Emitted when the contract is paused by admin. (#27) /// /// Compatibility decision: payload is `(true,)`. Empty-tuple expansion is @@ -1139,6 +1156,11 @@ impl SLACalculatorContract { ); env.storage().instance().set(&CONFIG_KEY, &configs); + // #455 – Seed CUSTOM_CONFIG_KEY so fresh and migrated contracts + // have the same instance-storage key layout. + env.storage() + .instance() + .set(&CUSTOM_CONFIG_KEY, &Map::::new(&env)); Self::write_version(&env); Ok(()) } @@ -1369,7 +1391,10 @@ impl SLACalculatorContract { storage_estimation::get_storage_footprint_estimate(&env) } - /// Returns the estimated per-ledger rent cost in stroops based on storage footprint. + /// Returns an approximate per-ledger rent cost in stroops based on storage footprint. + /// + /// **Note (#459):** This is a relative growth proxy, not an authoritative + /// rent figure. See `storage_estimation::get_rent_estimate` for details. pub fn get_rent_estimate(env: Env) -> Result { storage_estimation::get_rent_estimate(&env) } @@ -1592,6 +1617,12 @@ impl SLACalculatorContract { .get(&CUSTOM_CONFIG_KEY) .unwrap_or_else(|| Map::new(&env)); + // #456 – Determine whether this is a first registration or a + // reconfiguration so the emitted event distinguishes the two + // lifecycle transitions. Indexers reconstructing the custom-severity + // set from events need this to tell "who added" from "who changed". + let is_update = custom.contains_key(severity.clone()); + custom.set( severity.clone(), SLAConfig { @@ -1606,8 +1637,18 @@ impl SLACalculatorContract { // #408 – record the config snapshot under its new version hash. Self::record_config_registry(&env)?; + // Emit the lifecycle-appropriate event: sev_add for first + // registration, sev_upd for reconfiguration. The payload shape + // is identical (threshold, penalty, reward) so consumers that only + // care about values can parse either; consumers that need the + // lifecycle distinction check topic[0]. + let event_name = if is_update { + EVENT_SEV_UPD + } else { + EVENT_SEV_ADD + }; env.events().publish( - (EVENT_CONFIG_UPD, EVENT_VERSION, severity), + (event_name, EVENT_VERSION, severity), (threshold_minutes, penalty_per_minute, reward_base), ); Ok(()) @@ -2076,12 +2117,12 @@ impl SLACalculatorContract { methods.push_back(method("propose_operator", true, "admin", "op_prop")); methods.push_back(method("prune_history", true, "admin", "pruned")); methods.push_back(method("prune_history_by_age", true, "admin", "pruned_a")); - methods.push_back(method("remove_custom_severity", true, "admin", "cfg_upd")); + methods.push_back(method("remove_custom_severity", true, "admin", "cfg_rem")); methods.push_back(method("renounce_admin", true, "admin", "adm_ren")); methods.push_back(method("replay_calculate_sla", true, "operator", "sla_calc")); // Setters: methods.push_back(method("set_config", true, "admin", "cfg_upd")); - methods.push_back(method("set_custom_severity", true, "admin", "cfg_upd")); + methods.push_back(method("set_custom_severity", true, "admin", "sev_add")); methods.push_back(method("set_operator", true, "admin", "op_set")); methods.push_back(method("set_retention_limit", true, "admin", "")); methods.push_back(method("unfreeze_config", true, "admin", "cfg_unfrz")); diff --git a/apexchainx_calculator/src/storage_estimation.rs b/apexchainx_calculator/src/storage_estimation.rs index b5aeebb..61075bb 100644 --- a/apexchainx_calculator/src/storage_estimation.rs +++ b/apexchainx_calculator/src/storage_estimation.rs @@ -36,12 +36,22 @@ pub fn get_storage_footprint_estimate(env: &Env) -> Result { Ok(footprint) } -/// Calculates the estimated rent cost (in stroops / smallest units) per ledger -/// based on current storage footprint byte size. +/// Calculates an **approximate** per-ledger storage rent cost (in stroops) +/// based on the current storage footprint. +/// +/// **Disclaimer (#459):** This is a relative growth proxy, not an +/// authoritative rent figure. The formula (`footprint / 10 + 1`) is a +/// placeholder approximation. Actual Stellar rent depends on network +/// parameters (rent fee per byte per ledger, minimum rent, etc.) that +/// are not available to the Soroban host in this SDK version. +/// +/// Operators should use this value to track **relative** storage cost +/// growth over time, not as an absolute budgeting number. pub fn get_rent_estimate(env: &Env) -> Result { crate::SLACalculatorContract::check_version(env)?; let footprint = get_storage_footprint_estimate(env)? as i128; - // Formula: ~1 stroop per 10 bytes per ledger + 1 base stroop + // Relative proxy: ~1 stroop per 10 bytes per ledger + 1 base stroop. + // See doc comment — this is not derived from network parameters. let rent_per_ledger = (footprint / 10) + 1; Ok(rent_per_ledger) } diff --git a/apexchainx_calculator/src/tests.rs b/apexchainx_calculator/src/tests.rs index 32b76b9..11b4a46 100644 --- a/apexchainx_calculator/src/tests.rs +++ b/apexchainx_calculator/src/tests.rs @@ -9243,12 +9243,12 @@ fn test_overwrite_existing_custom_severity_emits_event() { assert_eq!(cfg2.penalty_per_minute, 75); assert_eq!(cfg2.reward_base, 600); - // Verify EVENT_CONFIG_UPD event was emitted for the update + // Verify EVENT_SEV_UPD event was emitted for the update (not cfg_upd) let events = env.events().all(); let (_, topics, data) = events.last().unwrap(); let topic_0: Symbol = topics.get(0).unwrap().try_into_val(&env).unwrap(); let topic_2: Symbol = topics.get(2).unwrap().try_into_val(&env).unwrap(); - assert_eq!(topic_0, EVENT_CONFIG_UPD); + assert_eq!(topic_0, EVENT_SEV_UPD); assert_eq!(topic_2, custom_sev); let payload: (u32, i128, i128) = data.try_into_val(&env).unwrap(); assert_eq!(payload, (15u32, 75i128, 600i128)); diff --git a/apexchainx_calculator/src/ts_parity_fixtures.rs b/apexchainx_calculator/src/ts_parity_fixtures.rs index fbf8677..bec56eb 100644 --- a/apexchainx_calculator/src/ts_parity_fixtures.rs +++ b/apexchainx_calculator/src/ts_parity_fixtures.rs @@ -105,6 +105,8 @@ fn event_topic_table() -> StdVec<(&'static str, &'static str, Symbol)> { ("duplicateInput", "dup_input", crate::EVENT_DUP_INPUT), ("configUpdated", "cfg_upd", crate::EVENT_CONFIG_UPD), ("configRemoved", "cfg_rem", crate::EVENT_CONFIG_REM), + ("severityAdded", "sev_add", crate::EVENT_SEV_ADD), + ("severityUpdated", "sev_upd", crate::EVENT_SEV_UPD), ("pruned", "pruned", crate::EVENT_PRUNED), ("prunedByAge", "pruned_a", crate::EVENT_PRUNED_AGE), ("retentionLimitSet", "ret_lim", crate::EVENT_RET_LIM), diff --git a/docs/AUDIT_TRAIL.md b/docs/AUDIT_TRAIL.md index beca8a6..97fae46 100644 --- a/docs/AUDIT_TRAIL.md +++ b/docs/AUDIT_TRAIL.md @@ -331,14 +331,19 @@ reconciliation alongside `sla_calc`): ### Configuration update: `cfg_upd` -Emitted by `set_config`, `set_custom_severity`, and -`remove_custom_severity`. The `cfg_upd` event always carries the +Emitted by `set_config`. The `cfg_upd` event always carries the **post-write** values. -When `remove_custom_severity` succeeds, the emitted `cfg_upd` payload -uses zeros (so indexers can distinguish a deletion from a "set to -zero" — see [`CHANGELOG.md`](../CHANGELOG.md) for the explicit -contract on this). +### Custom severity lifecycle: `sev_add`, `sev_upd`, `cfg_rem` + +- `sev_add` — emitted when `set_custom_severity` registers a **new** custom severity. +- `sev_upd` — emitted when `set_custom_severity` **reconfigures** an existing one. +- `cfg_rem` — emitted when `remove_custom_severity` deletes a custom severity. + +All three carry the custom severity symbol in topic[2]. `sev_add` and +`sev_upd` carry the post-write config triple; `cfg_rem` carries an empty +payload. Indexers can reconstruct the registered custom-severity set by +tracking `sev_add` (add) and `cfg_rem` (remove) events. | Field | Type | Description | |-------|------|-------------| diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index b92f6b6..8af2e3e 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -295,7 +295,7 @@ used in startup probes or cache-warming pipelines. | `pause` / `unpause` | `admin` | Writes `PAUSED`/`PAUSEINF`, emits `paused`/`unpause`. | | `freeze_config` / `unfreeze_config` | `admin` | Delegates to `config_freeze`, emits `cfg_frz`/`cfg_unfrz`. | | `set_config` | `admin` | Validates + writes `CONFIG_KEY`, stamps `LAST_CFG_UPDATE`, emits `cfg_upd`. | -| `set_custom_severity` / `remove_custom_severity` | `admin` | Mutates `CUSTOM_CONFIG_KEY`, emits `cfg_upd`. | +| `set_custom_severity` / `remove_custom_severity` | `admin` | Mutates `CUSTOM_CONFIG_KEY`, emits `sev_add`/`sev_upd`/`cfg_rem`. | | `calculate_sla` | `operator` | Writes history, stats, telemetry; emits `sla_calc` + `set_int`. | | `set_retention_limit` | `admin` | Writes `RETLIM`. | | `prune_history` / `prune_history_by_age` | `admin` | Truncates `HIST`, emits `pruned`/`pruned_a`. |