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
28 changes: 21 additions & 7 deletions apexchainx_calculator/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -104,10 +104,15 @@ pub fn get_last_config_update(env: &Env) -> Result<Option<crate::ConfigUpdateInf
/// Registers or updates a custom (non-canonical) severity level.
///
/// # Overwrite & Lifecycle Behavior
/// - If a custom severity with the given symbol is not registered, it is added to `CUSTCFG`.
/// - If a custom severity with the given symbol already exists, calling `set_custom_severity`
/// overwrites the existing parameters (`threshold_minutes`, `penalty_per_minute`, `reward_base`) in-place.
/// - In both cases, a `cfg_upd` (`EVENT_CONFIG_UPD`) event is emitted with the severity symbol and parameters.
/// - If a custom severity with the given symbol is not registered, a `sev_add`
/// (`EVENT_SEV_ADD`) event is emitted — indexers can reconstruct the
/// registered set from these creation events alone.
/// - If a custom severity with the given symbol already exists, a `sev_upd`
/// (`EVENT_SEV_UPD`) event is emitted — indexers can tell reconfiguration
/// from first registration by the distinct event name.
/// - The payload shape is identical in both cases `(threshold_minutes,
/// penalty_per_minute, reward_base)` so consumers that only care about
/// values can parse either event.
pub fn set_custom_severity(
env: &Env,
severity: Symbol,
Expand All @@ -134,6 +139,10 @@ pub fn set_custom_severity(
.get(&CUSTOM_CONFIG_KEY)
.unwrap_or_else(|| Map::new(env));

// #456 – Determine lifecycle transition before writing so the emitted
// event distinguishes creation from reconfiguration.
let is_update = custom.contains_key(severity.clone());

custom.set(
severity.clone(),
SLAConfig {
Expand All @@ -144,8 +153,13 @@ pub fn set_custom_severity(
);
env.storage().instance().set(&CUSTOM_CONFIG_KEY, &custom);

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(())
Expand All @@ -170,7 +184,7 @@ pub fn remove_custom_severity(env: &Env, severity: Symbol) -> 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(())
}

Expand Down
27 changes: 27 additions & 0 deletions apexchainx_calculator/src/event_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Expand Down
49 changes: 45 additions & 4 deletions apexchainx_calculator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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::<Symbol, SLAConfig>::new(&env));
Self::write_version(&env);
Ok(())
}
Expand Down Expand Up @@ -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<i128, SLAError> {
storage_estimation::get_rent_estimate(&env)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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(())
Expand Down Expand Up @@ -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"));
Expand Down
16 changes: 13 additions & 3 deletions apexchainx_calculator/src/storage_estimation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,22 @@ pub fn get_storage_footprint_estimate(env: &Env) -> Result<u64, SLAError> {
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<i128, SLAError> {
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)
}
Expand Down
4 changes: 2 additions & 2 deletions apexchainx_calculator/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
2 changes: 2 additions & 0 deletions apexchainx_calculator/src/ts_parity_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
17 changes: 11 additions & 6 deletions docs/AUDIT_TRAIL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|-------|------|-------------|
Expand Down
2 changes: 1 addition & 1 deletion docs/PROJECT_CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`. |
Expand Down
Loading