From e8295f2852479950117e16cdf796fda4bb61b421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Wed, 2 Sep 2026 09:48:27 +0000 Subject: [PATCH 1/7] feat(ledger): DEFI-1565: expose archive and dedup settings as metrics The archiving configuration was not observable from outside either ledger: `archives()` and `ledger_num_archives` show which archives exist, but nothing showed the settings that govern when archiving happens. An alert that needs to compare against one of them has to hard-code it, and then drifts whenever `ChangeArchiveOptions` is used. Exposes the six numerical archive settings, plus the transaction deduplication settings, which were equally invisible. All are the effective values in force: `Archive::new` already resolves the optional `ArchiveOptions` fields, and `max_transactions_per_response` now resolves through a default shared with the archive canister so the value the ledger reports cannot drift from the one the archive enforces. Principals are deliberately left out; `controller_id` and `more_controller_ids` would need label-valued series and are out of scope. Co-Authored-By: Claude Opus 5 (1M context) --- .../ledger_canister_core/src/archive.rs | 13 ++++ rs/ledger_suite/icp/ledger/src/main.rs | 76 ++++++++++++++++++- rs/ledger_suite/icp/ledger/tests/tests.rs | 73 +++++++++++++++++- rs/ledger_suite/icrc1/archive/src/main.rs | 8 +- rs/ledger_suite/icrc1/ledger/src/main.rs | 76 +++++++++++++++++++ rs/ledger_suite/icrc1/ledger/tests/tests.rs | 8 ++ rs/ledger_suite/tests/sm-tests/src/lib.rs | 63 ++++++++++++++- 7 files changed, 308 insertions(+), 9 deletions(-) diff --git a/rs/ledger_suite/common/ledger_canister_core/src/archive.rs b/rs/ledger_suite/common/ledger_canister_core/src/archive.rs index d2dfa7643c53..7e10a5eb2703 100644 --- a/rs/ledger_suite/common/ledger_canister_core/src/archive.rs +++ b/rs/ledger_suite/common/ledger_canister_core/src/archive.rs @@ -15,6 +15,12 @@ use ic_ledger_core::block::EncodedBlock; /// 10 trillion cycles. pub const DEFAULT_CYCLES_FOR_ARCHIVE_CREATION: u64 = 10_000_000_000_000; +/// The default maximum number of transactions returned by an archive's +/// `get_transactions` endpoint, applied when `max_transactions_per_response` is +/// not set. Shared with the archive canister so that the effective value the +/// ledger reports cannot drift from the one the archive enforces. +pub const DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE: u64 = 2000; + fn default_cycles_for_archive_creation() -> u64 { 0 } @@ -199,6 +205,13 @@ impl Archive { pub fn nodes(&self) -> &[CanisterId] { &self.nodes } + + /// The maximum number of transactions an archive of this ledger returns per + /// response, with the default applied when the option is unset. + pub fn effective_max_transactions_per_response(&self) -> u64 { + self.max_transactions_per_response + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE) + } } /// Grabs a write lock on the archive and executes a synchronous function under the lock. diff --git a/rs/ledger_suite/icp/ledger/src/main.rs b/rs/ledger_suite/icp/ledger/src/main.rs index c340a3048d09..62ea6d8fc39b 100644 --- a/rs/ledger_suite/icp/ledger/src/main.rs +++ b/rs/ledger_suite/icp/ledger/src/main.rs @@ -14,9 +14,9 @@ use ic_cdk::{post_upgrade, pre_upgrade, query, update}; use ic_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; use ic_icrc1::endpoints::{StandardRecord, convert_transfer_error}; use ic_ledger_canister_core::ledger::{LedgerContext, LedgerData}; -use ic_ledger_canister_core::runtime::heap_memory_size_bytes; +use ic_ledger_canister_core::runtime::{Runtime, heap_memory_size_bytes}; use ic_ledger_canister_core::{ - archive::{Archive, ArchiveOptions}, + archive::{Archive, ArchiveCanisterWasm, ArchiveOptions}, ledger::{ LedgerAccess, TransferError as CoreTransferError, apply_transaction, archive_blocks, block_locations, find_block_in_archive, @@ -1130,6 +1130,74 @@ fn get_nodes_() { }) } +/// Exposes the archiving configuration that is otherwise not observable from +/// outside the canister. All values are the effective ones in force, i.e. with +/// the defaults of the optional `ArchiveOptions` fields already applied. +fn encode_archive_config_metrics( + w: &mut ic_metrics_encoder::MetricsEncoder>, + archive: &Archive, +) -> std::io::Result<()> +where + Rt: Runtime, + Wasm: ArchiveCanisterWasm, +{ + w.encode_gauge( + "ledger_archive_trigger_threshold", + archive.trigger_threshold as f64, + "The number of blocks which, when exceeded, triggers archiving.", + )?; + w.encode_gauge( + "ledger_archive_num_blocks_to_archive", + archive.num_blocks_to_archive as f64, + "The number of blocks archived when the trigger threshold is exceeded.", + )?; + w.encode_gauge( + "ledger_archive_node_max_memory_size_bytes", + archive.node_max_memory_size_bytes as f64, + "Maximum number of bytes an archive canister of this ledger may store.", + )?; + w.encode_gauge( + "ledger_archive_max_message_size_bytes", + archive.max_message_size_bytes as f64, + "Maximum size in bytes of a message sent to an archive canister.", + )?; + w.encode_gauge( + "ledger_archive_cycles_for_archive_creation", + archive.cycles_for_archive_creation as f64, + "Cycles attached to the call creating a new archive canister.", + )?; + w.encode_gauge( + "ledger_archive_max_transactions_per_response", + archive.effective_max_transactions_per_response() as f64, + "Maximum number of transactions an archive returns per response.", + )?; + Ok(()) +} + +/// Exposes the transaction-deduplication configuration, which is not observable +/// from outside the canister. +fn encode_dedup_config_metrics( + w: &mut ic_metrics_encoder::MetricsEncoder>, + ledger: &LD, +) -> std::io::Result<()> { + w.encode_gauge( + "ledger_transaction_window_seconds", + ledger.transaction_window().as_secs() as f64, + "Length of the transaction deduplication window in seconds.", + )?; + w.encode_gauge( + "ledger_max_transactions_in_window", + ledger.max_transactions_in_window() as f64, + "Maximum number of transactions retained in the deduplication window.", + )?; + w.encode_gauge( + "ledger_max_transactions_to_purge", + ledger.max_transactions_to_purge() as f64, + "Maximum number of transactions purged from the deduplication window per operation.", + )?; + Ok(()) +} + fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::io::Result<()> { let ledger = LEDGER .try_read() @@ -1144,6 +1212,10 @@ fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::i *MAX_MESSAGE_SIZE_BYTES.read().unwrap() as f64, "Maximum inter-canister message size in bytes.", )?; + if let Some(archive) = archive_guard.as_ref() { + encode_archive_config_metrics(w, archive)?; + } + encode_dedup_config_metrics(w, &*ledger)?; w.encode_gauge( "ledger_stable_memory_pages", ic_cdk::stable::stable_size() as f64, diff --git a/rs/ledger_suite/icp/ledger/tests/tests.rs b/rs/ledger_suite/icp/ledger/tests/tests.rs index 51c89ce3f055..46f5b168227f 100644 --- a/rs/ledger_suite/icp/ledger/tests/tests.rs +++ b/rs/ledger_suite/icp/ledger/tests/tests.rs @@ -8,8 +8,8 @@ use ic_icrc1_test_utils::minter_identity; use ic_ledger_core::block::BlockIndex; use ic_ledger_core::{Tokens, block::BlockType}; use ic_ledger_suite_state_machine_helpers::{ - AllowanceProvider, balance_of, icrc21_consent_message, send_approval, send_transfer, - send_transfer_from, supported_standards, total_supply, transfer, + AllowanceProvider, balance_of, icrc21_consent_message, parse_metric, send_approval, + send_transfer, send_transfer_from, supported_standards, total_supply, transfer, }; use ic_ledger_suite_state_machine_tests::archiving::icp_archives; use ic_ledger_suite_state_machine_tests::{ @@ -2624,6 +2624,75 @@ fn test_burn_whole_balance() { assert_eq!(balance_of(&env, canister_id, p1.0), 0); } +#[test] +fn test_archive_and_dedup_config_metrics() { + const TRIGGER_THRESHOLD: usize = 17; + const NUM_BLOCKS_TO_ARCHIVE: usize = 5; + const NODE_MAX_MEMORY_SIZE_BYTES: u64 = 123_456; + const MAX_MESSAGE_SIZE_BYTES: u64 = 64 * 1024; + const CYCLES_FOR_ARCHIVE_CREATION: u64 = 7_000_000_000; + const MAX_TRANSACTIONS_PER_RESPONSE: u64 = 99; + const TRANSACTION_WINDOW: Duration = Duration::from_secs(3600); + + let env = StateMachine::new(); + let payload = LedgerCanisterInitPayload::builder() + .minting_account(MINTER.into()) + .icrc1_minting_account(MINTER) + .transfer_fee(Tokens::from_e8s(10_000)) + .token_symbol_and_name("ICP", "Internet Computer") + .transaction_window(TRANSACTION_WINDOW) + .archive_options(ArchiveOptions { + trigger_threshold: TRIGGER_THRESHOLD, + num_blocks_to_archive: NUM_BLOCKS_TO_ARCHIVE, + node_max_memory_size_bytes: Some(NODE_MAX_MEMORY_SIZE_BYTES), + max_message_size_bytes: Some(MAX_MESSAGE_SIZE_BYTES), + controller_id: PrincipalId::new_user_test_id(100), + more_controller_ids: None, + cycles_for_archive_creation: Some(CYCLES_FOR_ARCHIVE_CREATION), + max_transactions_per_response: Some(MAX_TRANSACTIONS_PER_RESPONSE), + }) + .build() + .unwrap(); + let ledger_id = env + .install_canister(ledger_wasm(), Encode!(&payload).unwrap(), None) + .expect("Unable to install the Ledger canister"); + + let metric = |name: &str| parse_metric(&env, ledger_id, name); + + assert_eq!( + metric("ledger_archive_trigger_threshold"), + TRIGGER_THRESHOLD as u64 + ); + assert_eq!( + metric("ledger_archive_num_blocks_to_archive"), + NUM_BLOCKS_TO_ARCHIVE as u64 + ); + assert_eq!( + metric("ledger_archive_node_max_memory_size_bytes"), + NODE_MAX_MEMORY_SIZE_BYTES + ); + assert_eq!( + metric("ledger_archive_max_message_size_bytes"), + MAX_MESSAGE_SIZE_BYTES + ); + assert_eq!( + metric("ledger_archive_cycles_for_archive_creation"), + CYCLES_FOR_ARCHIVE_CREATION + ); + assert_eq!( + metric("ledger_archive_max_transactions_per_response"), + MAX_TRANSACTIONS_PER_RESPONSE + ); + + assert_eq!( + metric("ledger_transaction_window_seconds"), + TRANSACTION_WINDOW.as_secs(), + "the ICP ledger's transaction window is configurable, so the metric must reflect the configured value" + ); + assert!(metric("ledger_max_transactions_in_window") > 0); + assert!(metric("ledger_max_transactions_to_purge") > 0); +} + #[test] fn test_change_initially_set_archive_options() { const ARCHIVE_TRIGGER_THRESHOLD: usize = 10; diff --git a/rs/ledger_suite/icrc1/archive/src/main.rs b/rs/ledger_suite/icrc1/archive/src/main.rs index e466303e3d89..c5a322d66340 100644 --- a/rs/ledger_suite/icrc1/archive/src/main.rs +++ b/rs/ledger_suite/icrc1/archive/src/main.rs @@ -37,7 +37,7 @@ const GIB: u64 = 1024 * 1024 * 1024; const DEFAULT_MEMORY_LIMIT: u64 = 3 * GIB; /// The maximum number of blocks to return in a single get_transactions request. -const DEFAULT_MAX_TRANSACTIONS_PER_GET_TRANSACTION_RESPONSE: u64 = 2000; +use ic_ledger_canister_core::archive::DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE; /// The maximum number of Wasm pages that we allow to use for the stable storage. const NUM_WASM_PAGES: u64 = 4 * GIB / WASM_PAGE_SIZE; @@ -108,7 +108,7 @@ impl Default for ArchiveConfig { max_memory_size_bytes: 0, block_index_offset: 0, ledger_id: Principal::management_canister(), - max_transactions_per_response: DEFAULT_MAX_TRANSACTIONS_PER_GET_TRANSACTION_RESPONSE, + max_transactions_per_response: DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE, token_type: wasm_token_type(), } } @@ -165,8 +165,8 @@ fn init( let max_memory_size_bytes = max_memory_size_bytes .unwrap_or(DEFAULT_MEMORY_LIMIT) .min(DEFAULT_MEMORY_LIMIT); - let max_transactions_per_response = max_transactions_per_response - .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_GET_TRANSACTION_RESPONSE); + let max_transactions_per_response = + max_transactions_per_response.unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE); cell.borrow_mut() .set(ArchiveConfig { max_memory_size_bytes, diff --git a/rs/ledger_suite/icrc1/ledger/src/main.rs b/rs/ledger_suite/icrc1/ledger/src/main.rs index 5ebdfe3f0361..f2f3d63f126d 100644 --- a/rs/ledger_suite/icrc1/ledger/src/main.rs +++ b/rs/ledger_suite/icrc1/ledger/src/main.rs @@ -17,10 +17,12 @@ use ic_icrc1_ledger::{ InitArgs, LEDGER_VERSION, Ledger, LedgerArgument, UPGRADES_MEMORY, balances_len, get_allowances, read_first_balance, wasm_token_type, }; +use ic_ledger_canister_core::archive::{Archive, ArchiveCanisterWasm}; use ic_ledger_canister_core::ledger::{ LedgerAccess, LedgerContext, LedgerData, TransferError as CoreTransferError, apply_transaction, archive_blocks, }; +use ic_ledger_canister_core::runtime::Runtime; use ic_ledger_canister_core::runtime::heap_memory_size_bytes; use ic_ledger_core::block::BlockIndex; use ic_ledger_core::timestamp::TimeStamp; @@ -284,6 +286,76 @@ fn log_message(msg: &str) { log!(&LOG, "{msg}"); } +/// Exposes the transaction-deduplication configuration, which is not observable +/// from outside the canister. The window is a constant on the ICRC ledger and a +/// configured value on the ICP ledger; both are reported here as the effective +/// value in force. +fn encode_dedup_config_metrics( + w: &mut ic_metrics_encoder::MetricsEncoder>, + ledger: &LD, +) -> std::io::Result<()> { + w.encode_gauge( + "ledger_transaction_window_seconds", + ledger.transaction_window().as_secs() as f64, + "Length of the transaction deduplication window in seconds.", + )?; + w.encode_gauge( + "ledger_max_transactions_in_window", + ledger.max_transactions_in_window() as f64, + "Maximum number of transactions retained in the deduplication window.", + )?; + w.encode_gauge( + "ledger_max_transactions_to_purge", + ledger.max_transactions_to_purge() as f64, + "Maximum number of transactions purged from the deduplication window per operation.", + )?; + Ok(()) +} + +/// Exposes the archiving configuration that is otherwise not observable from +/// outside the canister. All values are the effective ones in force, i.e. with +/// the defaults of the optional `ArchiveOptions` fields already applied. +fn encode_archive_config_metrics( + w: &mut ic_metrics_encoder::MetricsEncoder>, + archive: &Archive, +) -> std::io::Result<()> +where + Rt: Runtime, + Wasm: ArchiveCanisterWasm, +{ + w.encode_gauge( + "ledger_archive_trigger_threshold", + archive.trigger_threshold as f64, + "The number of blocks which, when exceeded, triggers archiving.", + )?; + w.encode_gauge( + "ledger_archive_num_blocks_to_archive", + archive.num_blocks_to_archive as f64, + "The number of blocks archived when the trigger threshold is exceeded.", + )?; + w.encode_gauge( + "ledger_archive_node_max_memory_size_bytes", + archive.node_max_memory_size_bytes as f64, + "Maximum number of bytes an archive canister of this ledger may store.", + )?; + w.encode_gauge( + "ledger_archive_max_message_size_bytes", + archive.max_message_size_bytes as f64, + "Maximum size in bytes of a message sent to an archive canister.", + )?; + w.encode_gauge( + "ledger_archive_cycles_for_archive_creation", + archive.cycles_for_archive_creation as f64, + "Cycles attached to the call creating a new archive canister.", + )?; + w.encode_gauge( + "ledger_archive_max_transactions_per_response", + archive.effective_max_transactions_per_response() as f64, + "Maximum number of transactions an archive returns per response.", + )?; + Ok(()) +} + fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::io::Result<()> { w.encode_gauge( "ledger_stable_memory_pages", @@ -402,6 +474,9 @@ fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::i num_archives as f64, "Total number of archives.", )?; + if let Some(archive) = archive_guard.as_ref() { + encode_archive_config_metrics(w, archive)?; + } } Err(err) => Err(std::io::Error::other(format!( "Failed to read number of archives: {err}" @@ -412,6 +487,7 @@ fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::i ledger.approvals().get_num_approvals() as f64, "Total number of approvals.", )?; + encode_dedup_config_metrics(w, &*ledger)?; Ok(()) }) } diff --git a/rs/ledger_suite/icrc1/ledger/tests/tests.rs b/rs/ledger_suite/icrc1/ledger/tests/tests.rs index 1d369b18f427..5e453debc951 100644 --- a/rs/ledger_suite/icrc1/ledger/tests/tests.rs +++ b/rs/ledger_suite/icrc1/ledger/tests/tests.rs @@ -242,6 +242,14 @@ fn test_change_trigger_threshold_before_archive_spawned() { ); } +#[test] +fn test_archive_and_dedup_config_metrics() { + ic_ledger_suite_state_machine_tests::test_archive_and_dedup_config_metrics( + ledger_wasm(), + encode_init_args, + ); +} + #[test] fn test_upgrade_archive_options() { ic_ledger_suite_state_machine_tests::test_upgrade_archive_options( diff --git a/rs/ledger_suite/tests/sm-tests/src/lib.rs b/rs/ledger_suite/tests/sm-tests/src/lib.rs index 2431be3a374c..c1d6699690b2 100644 --- a/rs/ledger_suite/tests/sm-tests/src/lib.rs +++ b/rs/ledger_suite/tests/sm-tests/src/lib.rs @@ -12,7 +12,7 @@ use ic_icrc1_ledger::FeatureFlags; use ic_icrc1_test_utils::{ ArgWithCaller, LedgerEndpointArg, icrc3::BlockBuilder, valid_transactions_strategy, }; -use ic_ledger_canister_core::archive::ArchiveOptions; +use ic_ledger_canister_core::archive::{ArchiveOptions, DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE}; use ic_ledger_core::block::{BlockIndex, BlockType, EncodedBlock}; use ic_ledger_core::timestamp::TimeStamp; use ic_ledger_core::tokens::TokensType; @@ -1455,6 +1455,67 @@ pub fn test_change_trigger_threshold_before_archive_spawned( ); } +pub fn test_archive_and_dedup_config_metrics( + ledger_wasm: Vec, + encode_init_args: fn(InitArgs) -> T, +) where + T: CandidType, +{ + let (env, ledger_id) = setup(ledger_wasm.clone(), encode_init_args, vec![]); + + let metric = |name: &str| parse_metric(&env, ledger_id, name); + + assert_eq!( + metric("ledger_archive_trigger_threshold"), + ARCHIVE_TRIGGER_THRESHOLD + ); + assert_eq!( + metric("ledger_archive_num_blocks_to_archive"), + NUM_BLOCKS_TO_ARCHIVE + ); + assert_eq!( + metric("ledger_archive_node_max_memory_size_bytes"), + 1024 * 1024 * 1024, + "setup() leaves node_max_memory_size_bytes unset, so the effective value should be the default" + ); + assert_eq!( + metric("ledger_archive_max_message_size_bytes"), + 2 * 1024 * 1024, + "setup() leaves max_message_size_bytes unset, so the effective value should be the default" + ); + assert_eq!(metric("ledger_archive_cycles_for_archive_creation"), 0); + assert_eq!( + metric("ledger_archive_max_transactions_per_response"), + DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE, + "setup() leaves max_transactions_per_response unset, so the effective value should be the default" + ); + + assert_eq!(metric("ledger_transaction_window_seconds"), 24 * 60 * 60); + assert!(metric("ledger_max_transactions_in_window") > 0); + assert!(metric("ledger_max_transactions_to_purge") > 0); + + let upgrade_args = LedgerArgument::Upgrade(Some(UpgradeArgs { + change_archive_options: Some(ChangeArchiveOptions { + trigger_threshold: Some(1234), + node_max_memory_size_bytes: Some(5678), + max_transactions_per_response: Some(42), + ..Default::default() + }), + ..UpgradeArgs::default() + })); + env.upgrade_canister(ledger_id, ledger_wasm, Encode!(&upgrade_args).unwrap()) + .expect("failed to change the archive options"); + + assert_eq!(metric("ledger_archive_trigger_threshold"), 1234); + assert_eq!(metric("ledger_archive_node_max_memory_size_bytes"), 5678); + assert_eq!(metric("ledger_archive_max_transactions_per_response"), 42); + assert_eq!( + metric("ledger_archive_num_blocks_to_archive"), + NUM_BLOCKS_TO_ARCHIVE, + "options not named in ChangeArchiveOptions must be left alone" + ); +} + pub fn test_upgrade_archive_options(ledger_wasm: Vec, encode_init_args: fn(InitArgs) -> T) where T: CandidType, From 94db22174a93923bc12cf57a79e1c70ab05acaba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Wed, 2 Sep 2026 10:09:42 +0000 Subject: [PATCH 2/7] fix(ledger): scope the archive settings metrics to new archives Three of the exposed settings only bear on archives the ledger spawns from now on: an archive that already exists was installed with whatever was configured at the time and keeps using it. The metric help text now says so and points at the archive's own metric for the value actually being enforced. Drops `ledger_archive_max_transactions_per_response` from the ICP ledger. The ICP archive's `init` takes only the ledger id, the block height offset and the memory cap, so the fourth argument the shared spawn code encodes is silently ignored and the setting has no effect there. The ICP archive already reported its own cap; the ICRC archive reported neither of its settings, so it now exposes both. That closes the gap: the ledger says what a new archive will get, each archive says what it enforces. Co-Authored-By: Claude Opus 5 (1M context) --- .../ledger_canister_core/src/archive.rs | 19 ++++-- rs/ledger_suite/icp/ledger/src/main.rs | 21 +++--- rs/ledger_suite/icp/ledger/tests/tests.rs | 16 +++-- rs/ledger_suite/icrc1/archive/src/main.rs | 13 ++++ rs/ledger_suite/icrc1/ledger/src/main.rs | 20 ++++-- rs/ledger_suite/icrc1/ledger/tests/tests.rs | 8 +++ rs/ledger_suite/tests/sm-tests/src/lib.rs | 64 +++++++++++++++++++ 7 files changed, 135 insertions(+), 26 deletions(-) diff --git a/rs/ledger_suite/common/ledger_canister_core/src/archive.rs b/rs/ledger_suite/common/ledger_canister_core/src/archive.rs index 7e10a5eb2703..a2c9bc28e97a 100644 --- a/rs/ledger_suite/common/ledger_canister_core/src/archive.rs +++ b/rs/ledger_suite/common/ledger_canister_core/src/archive.rs @@ -15,10 +15,17 @@ use ic_ledger_core::block::EncodedBlock; /// 10 trillion cycles. pub const DEFAULT_CYCLES_FOR_ARCHIVE_CREATION: u64 = 10_000_000_000_000; -/// The default maximum number of transactions returned by an archive's +/// The default maximum number of transactions returned by an ICRC archive's /// `get_transactions` endpoint, applied when `max_transactions_per_response` is -/// not set. Shared with the archive canister so that the effective value the -/// ledger reports cannot drift from the one the archive enforces. +/// not set. Shared with the ICRC archive canister so that the value the ledger +/// would pass to a new archive cannot drift from the one that archive applies. +/// +/// This bears only on archives the ledger spawns from now on. An archive that +/// already exists was installed with whatever value was configured, and with +/// whatever default the archive Wasm of the day applied, and keeps using it. +/// The ICP archive has no such setting at all: its `init` takes only the ledger +/// id, the block height offset and the memory cap, so the fourth argument +/// `send_blocks_to_archive` encodes is silently ignored there. pub const DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE: u64 = 2000; fn default_cycles_for_archive_creation() -> u64 { @@ -206,8 +213,10 @@ impl Archive { &self.nodes } - /// The maximum number of transactions an archive of this ledger returns per - /// response, with the default applied when the option is unset. + /// The maximum number of transactions a *newly spawned* ICRC archive of this + /// ledger will return per response, with the default applied when the option + /// is unset. Existing archives are unaffected by later changes to this + /// setting; see [`DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE`]. pub fn effective_max_transactions_per_response(&self) -> u64 { self.max_transactions_per_response .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE) diff --git a/rs/ledger_suite/icp/ledger/src/main.rs b/rs/ledger_suite/icp/ledger/src/main.rs index 62ea6d8fc39b..88e2b271f5bc 100644 --- a/rs/ledger_suite/icp/ledger/src/main.rs +++ b/rs/ledger_suite/icp/ledger/src/main.rs @@ -1131,8 +1131,14 @@ fn get_nodes_() { } /// Exposes the archiving configuration that is otherwise not observable from -/// outside the canister. All values are the effective ones in force, i.e. with -/// the defaults of the optional `ArchiveOptions` fields already applied. +/// outside the canister, with the defaults of the optional `ArchiveOptions` +/// fields already applied. +/// +/// `trigger_threshold`, `num_blocks_to_archive` and `max_message_size_bytes` +/// govern the ledger's own behaviour and take effect immediately. The remaining +/// settings are only used when the ledger spawns a *new* archive: an archive +/// that already exists keeps the values it was installed with, and reports them +/// through its own metrics. fn encode_archive_config_metrics( w: &mut ic_metrics_encoder::MetricsEncoder>, archive: &Archive, @@ -1154,7 +1160,9 @@ where w.encode_gauge( "ledger_archive_node_max_memory_size_bytes", archive.node_max_memory_size_bytes as f64, - "Maximum number of bytes an archive canister of this ledger may store.", + "Maximum number of bytes an archive spawned from now on may store. Existing \ + archives keep the cap they were created with, reported by their own \ + archive_node_max_memory_size_bytes metric.", )?; w.encode_gauge( "ledger_archive_max_message_size_bytes", @@ -1164,12 +1172,7 @@ where w.encode_gauge( "ledger_archive_cycles_for_archive_creation", archive.cycles_for_archive_creation as f64, - "Cycles attached to the call creating a new archive canister.", - )?; - w.encode_gauge( - "ledger_archive_max_transactions_per_response", - archive.effective_max_transactions_per_response() as f64, - "Maximum number of transactions an archive returns per response.", + "Cycles that will be attached to the call creating the next archive canister.", )?; Ok(()) } diff --git a/rs/ledger_suite/icp/ledger/tests/tests.rs b/rs/ledger_suite/icp/ledger/tests/tests.rs index 46f5b168227f..c265832765ae 100644 --- a/rs/ledger_suite/icp/ledger/tests/tests.rs +++ b/rs/ledger_suite/icp/ledger/tests/tests.rs @@ -8,8 +8,8 @@ use ic_icrc1_test_utils::minter_identity; use ic_ledger_core::block::BlockIndex; use ic_ledger_core::{Tokens, block::BlockType}; use ic_ledger_suite_state_machine_helpers::{ - AllowanceProvider, balance_of, icrc21_consent_message, parse_metric, send_approval, - send_transfer, send_transfer_from, supported_standards, total_supply, transfer, + AllowanceProvider, balance_of, icrc21_consent_message, parse_metric, retrieve_metrics, + send_approval, send_transfer, send_transfer_from, supported_standards, total_supply, transfer, }; use ic_ledger_suite_state_machine_tests::archiving::icp_archives; use ic_ledger_suite_state_machine_tests::{ @@ -2631,7 +2631,6 @@ fn test_archive_and_dedup_config_metrics() { const NODE_MAX_MEMORY_SIZE_BYTES: u64 = 123_456; const MAX_MESSAGE_SIZE_BYTES: u64 = 64 * 1024; const CYCLES_FOR_ARCHIVE_CREATION: u64 = 7_000_000_000; - const MAX_TRANSACTIONS_PER_RESPONSE: u64 = 99; const TRANSACTION_WINDOW: Duration = Duration::from_secs(3600); let env = StateMachine::new(); @@ -2649,7 +2648,7 @@ fn test_archive_and_dedup_config_metrics() { controller_id: PrincipalId::new_user_test_id(100), more_controller_ids: None, cycles_for_archive_creation: Some(CYCLES_FOR_ARCHIVE_CREATION), - max_transactions_per_response: Some(MAX_TRANSACTIONS_PER_RESPONSE), + max_transactions_per_response: None, }) .build() .unwrap(); @@ -2679,9 +2678,12 @@ fn test_archive_and_dedup_config_metrics() { metric("ledger_archive_cycles_for_archive_creation"), CYCLES_FOR_ARCHIVE_CREATION ); - assert_eq!( - metric("ledger_archive_max_transactions_per_response"), - MAX_TRANSACTIONS_PER_RESPONSE + assert!( + !retrieve_metrics(&env, ledger_id) + .iter() + .any(|line| line.starts_with("ledger_archive_max_transactions_per_response")), + "the ICP archive has no max_transactions_per_response setting, so the ICP ledger must not \ + advertise one" ); assert_eq!( diff --git a/rs/ledger_suite/icrc1/archive/src/main.rs b/rs/ledger_suite/icrc1/archive/src/main.rs index c5a322d66340..1e9e6d90cdd6 100644 --- a/rs/ledger_suite/icrc1/archive/src/main.rs +++ b/rs/ledger_suite/icrc1/archive/src/main.rs @@ -423,6 +423,19 @@ fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::i w.gauge_vec("cycle_balance", "Cycle balance on this canister.")? .value(&[("canister", "icrc1-archive")], cycle_balance)?; + with_archive_opts(|opts| { + w.encode_gauge( + "archive_max_memory_size_bytes", + opts.max_memory_size_bytes as f64, + "Maximum number of bytes this archive can use to store encoded blocks.", + )?; + w.encode_gauge( + "archive_max_transactions_per_response", + opts.max_transactions_per_response as f64, + "Maximum number of transactions this archive returns per response.", + ) + })?; + w.encode_gauge( "archive_stored_blocks", with_blocks(|blocks| blocks.len()) as f64, diff --git a/rs/ledger_suite/icrc1/ledger/src/main.rs b/rs/ledger_suite/icrc1/ledger/src/main.rs index f2f3d63f126d..980f02872c35 100644 --- a/rs/ledger_suite/icrc1/ledger/src/main.rs +++ b/rs/ledger_suite/icrc1/ledger/src/main.rs @@ -313,8 +313,14 @@ fn encode_dedup_config_metrics( } /// Exposes the archiving configuration that is otherwise not observable from -/// outside the canister. All values are the effective ones in force, i.e. with -/// the defaults of the optional `ArchiveOptions` fields already applied. +/// outside the canister, with the defaults of the optional `ArchiveOptions` +/// fields already applied. +/// +/// `trigger_threshold`, `num_blocks_to_archive` and `max_message_size_bytes` +/// govern the ledger's own behaviour and take effect immediately. The remaining +/// settings are only used when the ledger spawns a *new* archive: an archive +/// that already exists keeps the values it was installed with, and reports them +/// through its own metrics. fn encode_archive_config_metrics( w: &mut ic_metrics_encoder::MetricsEncoder>, archive: &Archive, @@ -336,7 +342,9 @@ where w.encode_gauge( "ledger_archive_node_max_memory_size_bytes", archive.node_max_memory_size_bytes as f64, - "Maximum number of bytes an archive canister of this ledger may store.", + "Maximum number of bytes an archive spawned from now on may store. Existing \ + archives keep the cap they were created with, reported by their own \ + archive_max_memory_size_bytes metric.", )?; w.encode_gauge( "ledger_archive_max_message_size_bytes", @@ -346,12 +354,14 @@ where w.encode_gauge( "ledger_archive_cycles_for_archive_creation", archive.cycles_for_archive_creation as f64, - "Cycles attached to the call creating a new archive canister.", + "Cycles that will be attached to the call creating the next archive canister.", )?; w.encode_gauge( "ledger_archive_max_transactions_per_response", archive.effective_max_transactions_per_response() as f64, - "Maximum number of transactions an archive returns per response.", + "Maximum number of transactions an archive spawned from now on will return \ + per response. Existing archives keep the limit they were created with, \ + reported by their own archive_max_transactions_per_response metric.", )?; Ok(()) } diff --git a/rs/ledger_suite/icrc1/ledger/tests/tests.rs b/rs/ledger_suite/icrc1/ledger/tests/tests.rs index 5e453debc951..28f33097d67a 100644 --- a/rs/ledger_suite/icrc1/ledger/tests/tests.rs +++ b/rs/ledger_suite/icrc1/ledger/tests/tests.rs @@ -242,6 +242,14 @@ fn test_change_trigger_threshold_before_archive_spawned() { ); } +#[test] +fn test_archive_reports_its_own_config_metrics() { + ic_ledger_suite_state_machine_tests::test_archive_reports_its_own_config_metrics( + ledger_wasm(), + encode_init_args, + ); +} + #[test] fn test_archive_and_dedup_config_metrics() { ic_ledger_suite_state_machine_tests::test_archive_and_dedup_config_metrics( diff --git a/rs/ledger_suite/tests/sm-tests/src/lib.rs b/rs/ledger_suite/tests/sm-tests/src/lib.rs index c1d6699690b2..03ee5625bca1 100644 --- a/rs/ledger_suite/tests/sm-tests/src/lib.rs +++ b/rs/ledger_suite/tests/sm-tests/src/lib.rs @@ -1516,6 +1516,70 @@ pub fn test_archive_and_dedup_config_metrics( ); } +/// The ledger's archive settings describe archives it will spawn from now on. +/// An archive that already exists keeps what it was installed with, so it has to +/// report its own effective settings. +pub fn test_archive_reports_its_own_config_metrics( + ledger_wasm: Vec, + encode_init_args: fn(InitArgs) -> T, +) where + T: CandidType, +{ + let p1 = PrincipalId::new_user_test_id(1); + let p2 = PrincipalId::new_user_test_id(2); + + let (env, ledger_id) = setup( + ledger_wasm.clone(), + encode_init_args, + vec![(Account::from(p1.0), 10_000_000)], + ); + for i in 0..ARCHIVE_TRIGGER_THRESHOLD { + transfer(&env, ledger_id, p1.0, p2.0, 10_000 + i).expect("transfer failed"); + } + let archives = list_archives(&env, ledger_id); + assert_eq!(archives.len(), 1); + let archive_id = CanisterId::unchecked_from_principal(archives[0].canister_id.into()); + + let ledger_cap = parse_metric(&env, ledger_id, "ledger_archive_node_max_memory_size_bytes"); + let archive_cap = parse_metric(&env, archive_id, "archive_max_memory_size_bytes"); + assert_eq!( + archive_cap, ledger_cap, + "the archive was just spawned, so its cap should match what the ledger would pass" + ); + assert_eq!( + parse_metric(&env, archive_id, "archive_max_transactions_per_response"), + DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE + ); + + // Changing the ledger's settings must not change what the existing archive + // enforces. + let upgrade_args = LedgerArgument::Upgrade(Some(UpgradeArgs { + change_archive_options: Some(ChangeArchiveOptions { + node_max_memory_size_bytes: Some(ledger_cap + 4096), + max_transactions_per_response: Some(7), + ..Default::default() + }), + ..UpgradeArgs::default() + })); + env.upgrade_canister(ledger_id, ledger_wasm, Encode!(&upgrade_args).unwrap()) + .expect("failed to change the archive options"); + + assert_eq!( + parse_metric(&env, ledger_id, "ledger_archive_node_max_memory_size_bytes"), + ledger_cap + 4096 + ); + assert_eq!( + parse_metric(&env, archive_id, "archive_max_memory_size_bytes"), + archive_cap, + "an existing archive keeps the cap it was installed with" + ); + assert_eq!( + parse_metric(&env, archive_id, "archive_max_transactions_per_response"), + DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE, + "an existing archive keeps the response limit it was installed with" + ); +} + pub fn test_upgrade_archive_options(ledger_wasm: Vec, encode_init_args: fn(InitArgs) -> T) where T: CandidType, From 925cb313a9d64e04231c23765e3484f2300980bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Wed, 2 Sep 2026 11:10:10 +0000 Subject: [PATCH 3/7] fix(ledger): report the archive limits that are actually enforced Two metrics claimed limits neither canister applies, both found in review. The message size limit is the archive option, but the size used when archiving is the smaller of it and the ledger's own limit. On an ICRC ledger that is the default case, not an edge case: the option defaults to 2 MiB while the ledger passes 1 MiB, so the metric was twice the truth everywhere. The help text now says which of the two it is, and the ICRC ledger exposes its own limit as well, so the smaller of the two can be seen. The ICP ledger already exposed its own. An ICRC archive both defaults to and clamps its memory cap at 3 GiB, so a larger value configured on the ledger has no effect. The ledger now reports the clamped value, through a limit shared with the archive so the two cannot disagree. The ICP archive has no such bound and is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .../ledger_canister_core/src/archive.rs | 9 +++++ rs/ledger_suite/icp/ledger/src/main.rs | 4 ++- rs/ledger_suite/icrc1/archive/src/main.rs | 2 +- rs/ledger_suite/icrc1/ledger/src/main.rs | 15 ++++++-- rs/ledger_suite/tests/sm-tests/src/lib.rs | 35 ++++++++++++++++--- 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/rs/ledger_suite/common/ledger_canister_core/src/archive.rs b/rs/ledger_suite/common/ledger_canister_core/src/archive.rs index a2c9bc28e97a..4ab2c547f3c7 100644 --- a/rs/ledger_suite/common/ledger_canister_core/src/archive.rs +++ b/rs/ledger_suite/common/ledger_canister_core/src/archive.rs @@ -28,6 +28,15 @@ pub const DEFAULT_CYCLES_FOR_ARCHIVE_CREATION: u64 = 10_000_000_000_000; /// `send_blocks_to_archive` encodes is silently ignored there. pub const DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE: u64 = 2000; +/// The hard upper bound an ICRC archive places on the number of bytes it will +/// use to store encoded blocks. Its `init` both defaults to and clamps to this +/// value, so a larger `node_max_memory_size_bytes` configured on the ledger has +/// no effect: the archive will still store at most this much. Shared so the +/// ledger can report the cap that will actually apply. +/// +/// The ICP archive has no equivalent bound; it takes whatever cap it is given. +pub const ICRC_ARCHIVE_MEMORY_LIMIT: u64 = 3 * 1024 * 1024 * 1024; + fn default_cycles_for_archive_creation() -> u64 { 0 } diff --git a/rs/ledger_suite/icp/ledger/src/main.rs b/rs/ledger_suite/icp/ledger/src/main.rs index 88e2b271f5bc..c0f6081a7a8f 100644 --- a/rs/ledger_suite/icp/ledger/src/main.rs +++ b/rs/ledger_suite/icp/ledger/src/main.rs @@ -1167,7 +1167,9 @@ where w.encode_gauge( "ledger_archive_max_message_size_bytes", archive.max_message_size_bytes as f64, - "Maximum size in bytes of a message sent to an archive canister.", + "Archive option limiting the size in bytes of a message sent to an archive. \ + The size actually used is the smaller of this and the ledger's own \ + ledger_max_message_size_bytes.", )?; w.encode_gauge( "ledger_archive_cycles_for_archive_creation", diff --git a/rs/ledger_suite/icrc1/archive/src/main.rs b/rs/ledger_suite/icrc1/archive/src/main.rs index 1e9e6d90cdd6..057ef4a0a3f9 100644 --- a/rs/ledger_suite/icrc1/archive/src/main.rs +++ b/rs/ledger_suite/icrc1/archive/src/main.rs @@ -34,7 +34,7 @@ const WASM_PAGE_SIZE: u64 = 65536; const GIB: u64 = 1024 * 1024 * 1024; /// How much memory do we want to allocate for raw blocks. -const DEFAULT_MEMORY_LIMIT: u64 = 3 * GIB; +use ic_ledger_canister_core::archive::ICRC_ARCHIVE_MEMORY_LIMIT as DEFAULT_MEMORY_LIMIT; /// The maximum number of blocks to return in a single get_transactions request. use ic_ledger_canister_core::archive::DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE; diff --git a/rs/ledger_suite/icrc1/ledger/src/main.rs b/rs/ledger_suite/icrc1/ledger/src/main.rs index 980f02872c35..33f80e48029a 100644 --- a/rs/ledger_suite/icrc1/ledger/src/main.rs +++ b/rs/ledger_suite/icrc1/ledger/src/main.rs @@ -17,7 +17,7 @@ use ic_icrc1_ledger::{ InitArgs, LEDGER_VERSION, Ledger, LedgerArgument, UPGRADES_MEMORY, balances_len, get_allowances, read_first_balance, wasm_token_type, }; -use ic_ledger_canister_core::archive::{Archive, ArchiveCanisterWasm}; +use ic_ledger_canister_core::archive::{Archive, ArchiveCanisterWasm, ICRC_ARCHIVE_MEMORY_LIMIT}; use ic_ledger_canister_core::ledger::{ LedgerAccess, LedgerContext, LedgerData, TransferError as CoreTransferError, apply_transaction, archive_blocks, @@ -341,7 +341,9 @@ where )?; w.encode_gauge( "ledger_archive_node_max_memory_size_bytes", - archive.node_max_memory_size_bytes as f64, + archive + .node_max_memory_size_bytes + .min(ICRC_ARCHIVE_MEMORY_LIMIT) as f64, "Maximum number of bytes an archive spawned from now on may store. Existing \ archives keep the cap they were created with, reported by their own \ archive_max_memory_size_bytes metric.", @@ -349,7 +351,9 @@ where w.encode_gauge( "ledger_archive_max_message_size_bytes", archive.max_message_size_bytes as f64, - "Maximum size in bytes of a message sent to an archive canister.", + "Archive option limiting the size in bytes of a message sent to an archive. \ + The size actually used is the smaller of this and the ledger's own \ + ledger_max_message_size_bytes.", )?; w.encode_gauge( "ledger_archive_cycles_for_archive_creation", @@ -497,6 +501,11 @@ fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::i ledger.approvals().get_num_approvals() as f64, "Total number of approvals.", )?; + w.encode_gauge( + "ledger_max_message_size_bytes", + MAX_MESSAGE_SIZE as f64, + "Maximum inter-canister message size in bytes.", + )?; encode_dedup_config_metrics(w, &*ledger)?; Ok(()) }) diff --git a/rs/ledger_suite/tests/sm-tests/src/lib.rs b/rs/ledger_suite/tests/sm-tests/src/lib.rs index 03ee5625bca1..10bdb61f1155 100644 --- a/rs/ledger_suite/tests/sm-tests/src/lib.rs +++ b/rs/ledger_suite/tests/sm-tests/src/lib.rs @@ -12,7 +12,9 @@ use ic_icrc1_ledger::FeatureFlags; use ic_icrc1_test_utils::{ ArgWithCaller, LedgerEndpointArg, icrc3::BlockBuilder, valid_transactions_strategy, }; -use ic_ledger_canister_core::archive::{ArchiveOptions, DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE}; +use ic_ledger_canister_core::archive::{ + ArchiveOptions, DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE, ICRC_ARCHIVE_MEMORY_LIMIT, +}; use ic_ledger_core::block::{BlockIndex, BlockType, EncodedBlock}; use ic_ledger_core::timestamp::TimeStamp; use ic_ledger_core::tokens::TokensType; @@ -1481,7 +1483,11 @@ pub fn test_archive_and_dedup_config_metrics( assert_eq!( metric("ledger_archive_max_message_size_bytes"), 2 * 1024 * 1024, - "setup() leaves max_message_size_bytes unset, so the effective value should be the default" + "setup() leaves max_message_size_bytes unset, so the archive option should be the default" + ); + assert!( + metric("ledger_max_message_size_bytes") <= metric("ledger_archive_max_message_size_bytes"), + "the size actually used when archiving is the smaller of the two, so both have to be visible" ); assert_eq!(metric("ledger_archive_cycles_for_archive_creation"), 0); assert_eq!( @@ -1503,12 +1509,33 @@ pub fn test_archive_and_dedup_config_metrics( }), ..UpgradeArgs::default() })); - env.upgrade_canister(ledger_id, ledger_wasm, Encode!(&upgrade_args).unwrap()) - .expect("failed to change the archive options"); + env.upgrade_canister( + ledger_id, + ledger_wasm.clone(), + Encode!(&upgrade_args).unwrap(), + ) + .expect("failed to change the archive options"); assert_eq!(metric("ledger_archive_trigger_threshold"), 1234); assert_eq!(metric("ledger_archive_node_max_memory_size_bytes"), 5678); assert_eq!(metric("ledger_archive_max_transactions_per_response"), 42); + + // An ICRC archive clamps its memory cap, so configuring more than it accepts + // must not be reported as if the archive would honour it. + let above_cap = LedgerArgument::Upgrade(Some(UpgradeArgs { + change_archive_options: Some(ChangeArchiveOptions { + node_max_memory_size_bytes: Some(ICRC_ARCHIVE_MEMORY_LIMIT * 2), + ..Default::default() + }), + ..UpgradeArgs::default() + })); + env.upgrade_canister(ledger_id, ledger_wasm, Encode!(&above_cap).unwrap()) + .expect("failed to change the archive options"); + assert_eq!( + metric("ledger_archive_node_max_memory_size_bytes"), + ICRC_ARCHIVE_MEMORY_LIMIT, + "the reported cap must not exceed what an ICRC archive will accept" + ); assert_eq!( metric("ledger_archive_num_blocks_to_archive"), NUM_BLOCKS_TO_ARCHIVE, From 5b0fa6234582572b442c3b2d1014911d9e8781ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Wed, 2 Sep 2026 11:36:18 +0000 Subject: [PATCH 4/7] fix(ledger): drop a needless reborrow flagged by clippy `with_ledger` already hands out a reference, so `&*ledger` there is a reborrow rather than a deref. Caught by `clippy::borrow_deref_ref` in the Cargo Lint job. Co-Authored-By: Claude Opus 5 (1M context) --- rs/ledger_suite/icrc1/ledger/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/ledger_suite/icrc1/ledger/src/main.rs b/rs/ledger_suite/icrc1/ledger/src/main.rs index 33f80e48029a..3d55c81022f0 100644 --- a/rs/ledger_suite/icrc1/ledger/src/main.rs +++ b/rs/ledger_suite/icrc1/ledger/src/main.rs @@ -506,7 +506,7 @@ fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::i MAX_MESSAGE_SIZE as f64, "Maximum inter-canister message size in bytes.", )?; - encode_dedup_config_metrics(w, &*ledger)?; + encode_dedup_config_metrics(w, ledger)?; Ok(()) }) } From 5730e47f1df9e849de007c0008911382e6199efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Wed, 2 Sep 2026 12:15:14 +0000 Subject: [PATCH 5/7] refactor(ledger): keep the ICRC archive limits in an ICRC crate Neither limit applies to the ICP ledger, so `ledger_canister_core` was the wrong home for them: the ICP archive has no `max_transactions_per_response` setting at all, and does not bound the memory cap it is given. Putting them there also left an ICRC-only accessor hanging off the shared `Archive` type. Moves both to `ic_icrc1::archive_limits`, which the ICRC ledger and the ICRC archive already depend on, and resolves the default and the clamp at the ledger's metric site instead. Co-Authored-By: Claude Opus 5 (1M context) --- .../ledger_canister_core/src/archive.rs | 31 ------------------- rs/ledger_suite/icrc1/archive/src/main.rs | 4 +-- rs/ledger_suite/icrc1/ledger/src/main.rs | 11 ++++--- rs/ledger_suite/icrc1/src/archive_limits.rs | 26 ++++++++++++++++ rs/ledger_suite/icrc1/src/lib.rs | 1 + rs/ledger_suite/tests/sm-tests/src/lib.rs | 9 +++--- 6 files changed, 39 insertions(+), 43 deletions(-) create mode 100644 rs/ledger_suite/icrc1/src/archive_limits.rs diff --git a/rs/ledger_suite/common/ledger_canister_core/src/archive.rs b/rs/ledger_suite/common/ledger_canister_core/src/archive.rs index 4ab2c547f3c7..d2dfa7643c53 100644 --- a/rs/ledger_suite/common/ledger_canister_core/src/archive.rs +++ b/rs/ledger_suite/common/ledger_canister_core/src/archive.rs @@ -15,28 +15,6 @@ use ic_ledger_core::block::EncodedBlock; /// 10 trillion cycles. pub const DEFAULT_CYCLES_FOR_ARCHIVE_CREATION: u64 = 10_000_000_000_000; -/// The default maximum number of transactions returned by an ICRC archive's -/// `get_transactions` endpoint, applied when `max_transactions_per_response` is -/// not set. Shared with the ICRC archive canister so that the value the ledger -/// would pass to a new archive cannot drift from the one that archive applies. -/// -/// This bears only on archives the ledger spawns from now on. An archive that -/// already exists was installed with whatever value was configured, and with -/// whatever default the archive Wasm of the day applied, and keeps using it. -/// The ICP archive has no such setting at all: its `init` takes only the ledger -/// id, the block height offset and the memory cap, so the fourth argument -/// `send_blocks_to_archive` encodes is silently ignored there. -pub const DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE: u64 = 2000; - -/// The hard upper bound an ICRC archive places on the number of bytes it will -/// use to store encoded blocks. Its `init` both defaults to and clamps to this -/// value, so a larger `node_max_memory_size_bytes` configured on the ledger has -/// no effect: the archive will still store at most this much. Shared so the -/// ledger can report the cap that will actually apply. -/// -/// The ICP archive has no equivalent bound; it takes whatever cap it is given. -pub const ICRC_ARCHIVE_MEMORY_LIMIT: u64 = 3 * 1024 * 1024 * 1024; - fn default_cycles_for_archive_creation() -> u64 { 0 } @@ -221,15 +199,6 @@ impl Archive { pub fn nodes(&self) -> &[CanisterId] { &self.nodes } - - /// The maximum number of transactions a *newly spawned* ICRC archive of this - /// ledger will return per response, with the default applied when the option - /// is unset. Existing archives are unaffected by later changes to this - /// setting; see [`DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE`]. - pub fn effective_max_transactions_per_response(&self) -> u64 { - self.max_transactions_per_response - .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE) - } } /// Grabs a write lock on the archive and executes a synchronous function under the lock. diff --git a/rs/ledger_suite/icrc1/archive/src/main.rs b/rs/ledger_suite/icrc1/archive/src/main.rs index 057ef4a0a3f9..6577841eb569 100644 --- a/rs/ledger_suite/icrc1/archive/src/main.rs +++ b/rs/ledger_suite/icrc1/archive/src/main.rs @@ -34,10 +34,10 @@ const WASM_PAGE_SIZE: u64 = 65536; const GIB: u64 = 1024 * 1024 * 1024; /// How much memory do we want to allocate for raw blocks. -use ic_ledger_canister_core::archive::ICRC_ARCHIVE_MEMORY_LIMIT as DEFAULT_MEMORY_LIMIT; +use ic_icrc1::archive_limits::ARCHIVE_MEMORY_LIMIT as DEFAULT_MEMORY_LIMIT; /// The maximum number of blocks to return in a single get_transactions request. -use ic_ledger_canister_core::archive::DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE; +use ic_icrc1::archive_limits::DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE; /// The maximum number of Wasm pages that we allow to use for the stable storage. const NUM_WASM_PAGES: u64 = 4 * GIB / WASM_PAGE_SIZE; diff --git a/rs/ledger_suite/icrc1/ledger/src/main.rs b/rs/ledger_suite/icrc1/ledger/src/main.rs index 3d55c81022f0..0c7e368c7dfe 100644 --- a/rs/ledger_suite/icrc1/ledger/src/main.rs +++ b/rs/ledger_suite/icrc1/ledger/src/main.rs @@ -9,6 +9,7 @@ use ic_cdk::init; use ic_cdk::stable::StableReader; use ic_cdk::{post_upgrade, pre_upgrade, query, update}; use ic_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; +use ic_icrc1::archive_limits::{ARCHIVE_MEMORY_LIMIT, DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE}; use ic_icrc1::{ Operation, Transaction, endpoints::{StandardRecord, convert_transfer_error}, @@ -17,7 +18,7 @@ use ic_icrc1_ledger::{ InitArgs, LEDGER_VERSION, Ledger, LedgerArgument, UPGRADES_MEMORY, balances_len, get_allowances, read_first_balance, wasm_token_type, }; -use ic_ledger_canister_core::archive::{Archive, ArchiveCanisterWasm, ICRC_ARCHIVE_MEMORY_LIMIT}; +use ic_ledger_canister_core::archive::{Archive, ArchiveCanisterWasm}; use ic_ledger_canister_core::ledger::{ LedgerAccess, LedgerContext, LedgerData, TransferError as CoreTransferError, apply_transaction, archive_blocks, @@ -341,9 +342,7 @@ where )?; w.encode_gauge( "ledger_archive_node_max_memory_size_bytes", - archive - .node_max_memory_size_bytes - .min(ICRC_ARCHIVE_MEMORY_LIMIT) as f64, + archive.node_max_memory_size_bytes.min(ARCHIVE_MEMORY_LIMIT) as f64, "Maximum number of bytes an archive spawned from now on may store. Existing \ archives keep the cap they were created with, reported by their own \ archive_max_memory_size_bytes metric.", @@ -362,7 +361,9 @@ where )?; w.encode_gauge( "ledger_archive_max_transactions_per_response", - archive.effective_max_transactions_per_response() as f64, + archive + .max_transactions_per_response + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE) as f64, "Maximum number of transactions an archive spawned from now on will return \ per response. Existing archives keep the limit they were created with, \ reported by their own archive_max_transactions_per_response metric.", diff --git a/rs/ledger_suite/icrc1/src/archive_limits.rs b/rs/ledger_suite/icrc1/src/archive_limits.rs new file mode 100644 index 000000000000..360928a2d476 --- /dev/null +++ b/rs/ledger_suite/icrc1/src/archive_limits.rs @@ -0,0 +1,26 @@ +//! Limits the ICRC archive canister applies to itself. +//! +//! These live here, rather than privately in the archive canister, because the +//! ledger has to report the values that will actually take effect: it decides +//! what to pass to a new archive, but the archive is what enforces them. Keeping +//! them in a crate both canisters depend on means the two cannot disagree. +//! +//! They are deliberately not in `ic_ledger_canister_core`, which the ICP ledger +//! also uses: neither limit applies there. The ICP archive has no +//! `max_transactions_per_response` setting at all, and does not bound the memory +//! cap it is given. + +/// The default maximum number of transactions returned by an ICRC archive's +/// `get_transactions` endpoint, applied when `max_transactions_per_response` is +/// not set. +/// +/// This bears only on archives the ledger spawns from now on. An archive that +/// already exists was installed with whatever value was configured, and with +/// whatever default the archive Wasm of the day applied, and keeps using it. +pub const DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE: u64 = 2000; + +/// The hard upper bound an ICRC archive places on the number of bytes it will +/// use to store encoded blocks. Its `init` both defaults to and clamps to this +/// value, so a larger `node_max_memory_size_bytes` configured on the ledger has +/// no effect: the archive will still store at most this much. +pub const ARCHIVE_MEMORY_LIMIT: u64 = 3 * 1024 * 1024 * 1024; diff --git a/rs/ledger_suite/icrc1/src/lib.rs b/rs/ledger_suite/icrc1/src/lib.rs index 9e22523ec16e..469ab2170aaa 100644 --- a/rs/ledger_suite/icrc1/src/lib.rs +++ b/rs/ledger_suite/icrc1/src/lib.rs @@ -1,3 +1,4 @@ +pub mod archive_limits; pub mod blocks; mod compact_account; pub mod endpoints; diff --git a/rs/ledger_suite/tests/sm-tests/src/lib.rs b/rs/ledger_suite/tests/sm-tests/src/lib.rs index 10bdb61f1155..6cf34840b421 100644 --- a/rs/ledger_suite/tests/sm-tests/src/lib.rs +++ b/rs/ledger_suite/tests/sm-tests/src/lib.rs @@ -6,15 +6,14 @@ use ic_base_types::PrincipalId; use ic_config::{execution_environment::Config as HypervisorConfig, subnet_config::SubnetConfig}; use ic_error_types::UserError; use ic_http_types::{HttpRequest, HttpResponse}; +use ic_icrc1::archive_limits::{ARCHIVE_MEMORY_LIMIT, DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE}; use ic_icrc1::blocks::{encoded_block_to_generic_block, generic_block_to_encoded_block}; use ic_icrc1::{Block, Operation, Transaction, hash::Hash}; use ic_icrc1_ledger::FeatureFlags; use ic_icrc1_test_utils::{ ArgWithCaller, LedgerEndpointArg, icrc3::BlockBuilder, valid_transactions_strategy, }; -use ic_ledger_canister_core::archive::{ - ArchiveOptions, DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE, ICRC_ARCHIVE_MEMORY_LIMIT, -}; +use ic_ledger_canister_core::archive::ArchiveOptions; use ic_ledger_core::block::{BlockIndex, BlockType, EncodedBlock}; use ic_ledger_core::timestamp::TimeStamp; use ic_ledger_core::tokens::TokensType; @@ -1524,7 +1523,7 @@ pub fn test_archive_and_dedup_config_metrics( // must not be reported as if the archive would honour it. let above_cap = LedgerArgument::Upgrade(Some(UpgradeArgs { change_archive_options: Some(ChangeArchiveOptions { - node_max_memory_size_bytes: Some(ICRC_ARCHIVE_MEMORY_LIMIT * 2), + node_max_memory_size_bytes: Some(ARCHIVE_MEMORY_LIMIT * 2), ..Default::default() }), ..UpgradeArgs::default() @@ -1533,7 +1532,7 @@ pub fn test_archive_and_dedup_config_metrics( .expect("failed to change the archive options"); assert_eq!( metric("ledger_archive_node_max_memory_size_bytes"), - ICRC_ARCHIVE_MEMORY_LIMIT, + ARCHIVE_MEMORY_LIMIT, "the reported cap must not exceed what an ICRC archive will accept" ); assert_eq!( From 7fe2575a482a5aa6accd7041231bed0f8d64211a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Wed, 2 Sep 2026 12:45:29 +0000 Subject: [PATCH 6/7] refactor(ledger): share the config metrics between the two ledgers Both ledgers had their own copy of the archive and deduplication metric encoders. `ic-metrics-encoder` is already a workspace dependency and already in the Bazel crate index, so `ledger_canister_core` can take it without repinning and hold the shared versions. The deduplication encoder was identical and moved as it was. The archive encoder was not: an ICRC archive clamps the memory cap it is given and has a response limit that the ICP archive does not, so the effective cap is a parameter and the ICRC-only gauge stays at its call site. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.lock | 1 + .../common/ledger_canister_core/BUILD.bazel | 1 + .../common/ledger_canister_core/Cargo.toml | 1 + .../common/ledger_canister_core/src/lib.rs | 1 + .../ledger_canister_core/src/metrics.rs | 88 +++++++++++++++ rs/ledger_suite/icp/ledger/src/main.rs | 82 +------------- rs/ledger_suite/icrc1/ledger/src/main.rs | 105 +++--------------- 7 files changed, 116 insertions(+), 163 deletions(-) create mode 100644 rs/ledger_suite/common/ledger_canister_core/src/metrics.rs diff --git a/Cargo.lock b/Cargo.lock index 42df1bad7bd5..ffd18607d150 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10520,6 +10520,7 @@ dependencies = [ "ic-ledger-hash-of", "ic-limits", "ic-management-canister-types-private", + "ic-metrics-encoder", "ic-stable-structures 0.6.9", "ic-utils 0.9.0", "serde", diff --git a/rs/ledger_suite/common/ledger_canister_core/BUILD.bazel b/rs/ledger_suite/common/ledger_canister_core/BUILD.bazel index b169802ec8aa..5ce15357d62a 100644 --- a/rs/ledger_suite/common/ledger_canister_core/BUILD.bazel +++ b/rs/ledger_suite/common/ledger_canister_core/BUILD.bazel @@ -22,6 +22,7 @@ rust_library( "//rs/utils", "@crate_index//:candid", "@crate_index//:ic-cdk", + "@crate_index//:ic-metrics-encoder", "@crate_index//:ic-stable-structures", "@crate_index//:serde", ], diff --git a/rs/ledger_suite/common/ledger_canister_core/Cargo.toml b/rs/ledger_suite/common/ledger_canister_core/Cargo.toml index e6d094978e91..02bd86223a4b 100644 --- a/rs/ledger_suite/common/ledger_canister_core/Cargo.toml +++ b/rs/ledger_suite/common/ledger_canister_core/Cargo.toml @@ -16,6 +16,7 @@ ic-limits = { path = "../../../limits" } ic-ledger-core = { path = "../ledger_core" } ic-ledger-hash-of = { path = "../../../../packages/ic-ledger-hash-of" } ic-management-canister-types-private = { path = "../../../types/management_canister_types" } +ic-metrics-encoder = { workspace = true } ic-stable-structures = { workspace = true } ic-utils = { path = "../../../utils" } serde = { workspace = true } diff --git a/rs/ledger_suite/common/ledger_canister_core/src/lib.rs b/rs/ledger_suite/common/ledger_canister_core/src/lib.rs index 8fe3786d95b1..cd02e7dd4878 100644 --- a/rs/ledger_suite/common/ledger_canister_core/src/lib.rs +++ b/rs/ledger_suite/common/ledger_canister_core/src/lib.rs @@ -1,6 +1,7 @@ pub mod archive; pub mod blockchain; pub mod ledger; +pub mod metrics; pub mod range_utils; pub mod runtime; mod spawn; diff --git a/rs/ledger_suite/common/ledger_canister_core/src/metrics.rs b/rs/ledger_suite/common/ledger_canister_core/src/metrics.rs new file mode 100644 index 000000000000..b08544ab2af3 --- /dev/null +++ b/rs/ledger_suite/common/ledger_canister_core/src/metrics.rs @@ -0,0 +1,88 @@ +//! Metrics that both ledgers expose identically. + +use crate::archive::{Archive, ArchiveCanisterWasm}; +use crate::ledger::LedgerData; +use crate::runtime::Runtime; +use ic_metrics_encoder::MetricsEncoder; + +/// Exposes the archiving configuration that is otherwise not observable from +/// outside the canister, with the defaults of the optional `ArchiveOptions` +/// fields already applied. +/// +/// `trigger_threshold`, `num_blocks_to_archive` and `max_message_size_bytes` +/// govern the ledger's own behaviour and take effect immediately. The memory cap +/// and the creation cycles are only used when the ledger spawns a *new* archive: +/// an archive that already exists keeps the values it was installed with, and +/// reports them through its own metrics. +/// +/// `effective_node_max_memory_size_bytes` is passed in because the two ledgers +/// resolve it differently: an ICRC archive clamps the cap it is given, an ICP +/// archive takes it as-is. The caller is the only one that knows which applies. +/// +/// `max_transactions_per_response` is deliberately not reported here. Only the +/// ICRC archive has that setting, so the ICRC ledger emits it at its call site. +pub fn encode_archive_config_metrics( + w: &mut MetricsEncoder>, + archive: &Archive, + effective_node_max_memory_size_bytes: u64, +) -> std::io::Result<()> +where + Rt: Runtime, + Wasm: ArchiveCanisterWasm, +{ + w.encode_gauge( + "ledger_archive_trigger_threshold", + archive.trigger_threshold as f64, + "The number of blocks which, when exceeded, triggers archiving.", + )?; + w.encode_gauge( + "ledger_archive_num_blocks_to_archive", + archive.num_blocks_to_archive as f64, + "The number of blocks archived when the trigger threshold is exceeded.", + )?; + w.encode_gauge( + "ledger_archive_node_max_memory_size_bytes", + effective_node_max_memory_size_bytes as f64, + "Maximum number of bytes an archive spawned from now on may store. Existing \ + archives keep the cap they were created with, reported by their own metric.", + )?; + w.encode_gauge( + "ledger_archive_max_message_size_bytes", + archive.max_message_size_bytes as f64, + "Archive option limiting the size in bytes of a message sent to an archive. \ + The size actually used is the smaller of this and the ledger's own \ + ledger_max_message_size_bytes.", + )?; + w.encode_gauge( + "ledger_archive_cycles_for_archive_creation", + archive.cycles_for_archive_creation as f64, + "Cycles that will be attached to the call creating the next archive canister.", + )?; + Ok(()) +} + +/// Exposes the transaction-deduplication configuration, which is not observable +/// from outside the canister. The window is a constant on the ICRC ledger and a +/// configured value on the ICP ledger; both are reported here as the effective +/// value in force. +pub fn encode_dedup_config_metrics( + w: &mut MetricsEncoder>, + ledger: &LD, +) -> std::io::Result<()> { + w.encode_gauge( + "ledger_transaction_window_seconds", + ledger.transaction_window().as_secs() as f64, + "Length of the transaction deduplication window in seconds.", + )?; + w.encode_gauge( + "ledger_max_transactions_in_window", + ledger.max_transactions_in_window() as f64, + "Maximum number of transactions retained in the deduplication window.", + )?; + w.encode_gauge( + "ledger_max_transactions_to_purge", + ledger.max_transactions_to_purge() as f64, + "Maximum number of transactions purged from the deduplication window per operation.", + )?; + Ok(()) +} diff --git a/rs/ledger_suite/icp/ledger/src/main.rs b/rs/ledger_suite/icp/ledger/src/main.rs index c0f6081a7a8f..cc988f0f5671 100644 --- a/rs/ledger_suite/icp/ledger/src/main.rs +++ b/rs/ledger_suite/icp/ledger/src/main.rs @@ -14,9 +14,12 @@ use ic_cdk::{post_upgrade, pre_upgrade, query, update}; use ic_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; use ic_icrc1::endpoints::{StandardRecord, convert_transfer_error}; use ic_ledger_canister_core::ledger::{LedgerContext, LedgerData}; -use ic_ledger_canister_core::runtime::{Runtime, heap_memory_size_bytes}; +use ic_ledger_canister_core::metrics::{ + encode_archive_config_metrics, encode_dedup_config_metrics, +}; +use ic_ledger_canister_core::runtime::heap_memory_size_bytes; use ic_ledger_canister_core::{ - archive::{Archive, ArchiveCanisterWasm, ArchiveOptions}, + archive::{Archive, ArchiveOptions}, ledger::{ LedgerAccess, TransferError as CoreTransferError, apply_transaction, archive_blocks, block_locations, find_block_in_archive, @@ -1130,79 +1133,6 @@ fn get_nodes_() { }) } -/// Exposes the archiving configuration that is otherwise not observable from -/// outside the canister, with the defaults of the optional `ArchiveOptions` -/// fields already applied. -/// -/// `trigger_threshold`, `num_blocks_to_archive` and `max_message_size_bytes` -/// govern the ledger's own behaviour and take effect immediately. The remaining -/// settings are only used when the ledger spawns a *new* archive: an archive -/// that already exists keeps the values it was installed with, and reports them -/// through its own metrics. -fn encode_archive_config_metrics( - w: &mut ic_metrics_encoder::MetricsEncoder>, - archive: &Archive, -) -> std::io::Result<()> -where - Rt: Runtime, - Wasm: ArchiveCanisterWasm, -{ - w.encode_gauge( - "ledger_archive_trigger_threshold", - archive.trigger_threshold as f64, - "The number of blocks which, when exceeded, triggers archiving.", - )?; - w.encode_gauge( - "ledger_archive_num_blocks_to_archive", - archive.num_blocks_to_archive as f64, - "The number of blocks archived when the trigger threshold is exceeded.", - )?; - w.encode_gauge( - "ledger_archive_node_max_memory_size_bytes", - archive.node_max_memory_size_bytes as f64, - "Maximum number of bytes an archive spawned from now on may store. Existing \ - archives keep the cap they were created with, reported by their own \ - archive_node_max_memory_size_bytes metric.", - )?; - w.encode_gauge( - "ledger_archive_max_message_size_bytes", - archive.max_message_size_bytes as f64, - "Archive option limiting the size in bytes of a message sent to an archive. \ - The size actually used is the smaller of this and the ledger's own \ - ledger_max_message_size_bytes.", - )?; - w.encode_gauge( - "ledger_archive_cycles_for_archive_creation", - archive.cycles_for_archive_creation as f64, - "Cycles that will be attached to the call creating the next archive canister.", - )?; - Ok(()) -} - -/// Exposes the transaction-deduplication configuration, which is not observable -/// from outside the canister. -fn encode_dedup_config_metrics( - w: &mut ic_metrics_encoder::MetricsEncoder>, - ledger: &LD, -) -> std::io::Result<()> { - w.encode_gauge( - "ledger_transaction_window_seconds", - ledger.transaction_window().as_secs() as f64, - "Length of the transaction deduplication window in seconds.", - )?; - w.encode_gauge( - "ledger_max_transactions_in_window", - ledger.max_transactions_in_window() as f64, - "Maximum number of transactions retained in the deduplication window.", - )?; - w.encode_gauge( - "ledger_max_transactions_to_purge", - ledger.max_transactions_to_purge() as f64, - "Maximum number of transactions purged from the deduplication window per operation.", - )?; - Ok(()) -} - fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::io::Result<()> { let ledger = LEDGER .try_read() @@ -1218,7 +1148,7 @@ fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::i "Maximum inter-canister message size in bytes.", )?; if let Some(archive) = archive_guard.as_ref() { - encode_archive_config_metrics(w, archive)?; + encode_archive_config_metrics(w, archive, archive.node_max_memory_size_bytes)?; } encode_dedup_config_metrics(w, &*ledger)?; w.encode_gauge( diff --git a/rs/ledger_suite/icrc1/ledger/src/main.rs b/rs/ledger_suite/icrc1/ledger/src/main.rs index 0c7e368c7dfe..99ebf0268e3e 100644 --- a/rs/ledger_suite/icrc1/ledger/src/main.rs +++ b/rs/ledger_suite/icrc1/ledger/src/main.rs @@ -18,12 +18,13 @@ use ic_icrc1_ledger::{ InitArgs, LEDGER_VERSION, Ledger, LedgerArgument, UPGRADES_MEMORY, balances_len, get_allowances, read_first_balance, wasm_token_type, }; -use ic_ledger_canister_core::archive::{Archive, ArchiveCanisterWasm}; use ic_ledger_canister_core::ledger::{ LedgerAccess, LedgerContext, LedgerData, TransferError as CoreTransferError, apply_transaction, archive_blocks, }; -use ic_ledger_canister_core::runtime::Runtime; +use ic_ledger_canister_core::metrics::{ + encode_archive_config_metrics, encode_dedup_config_metrics, +}; use ic_ledger_canister_core::runtime::heap_memory_size_bytes; use ic_ledger_core::block::BlockIndex; use ic_ledger_core::timestamp::TimeStamp; @@ -287,90 +288,6 @@ fn log_message(msg: &str) { log!(&LOG, "{msg}"); } -/// Exposes the transaction-deduplication configuration, which is not observable -/// from outside the canister. The window is a constant on the ICRC ledger and a -/// configured value on the ICP ledger; both are reported here as the effective -/// value in force. -fn encode_dedup_config_metrics( - w: &mut ic_metrics_encoder::MetricsEncoder>, - ledger: &LD, -) -> std::io::Result<()> { - w.encode_gauge( - "ledger_transaction_window_seconds", - ledger.transaction_window().as_secs() as f64, - "Length of the transaction deduplication window in seconds.", - )?; - w.encode_gauge( - "ledger_max_transactions_in_window", - ledger.max_transactions_in_window() as f64, - "Maximum number of transactions retained in the deduplication window.", - )?; - w.encode_gauge( - "ledger_max_transactions_to_purge", - ledger.max_transactions_to_purge() as f64, - "Maximum number of transactions purged from the deduplication window per operation.", - )?; - Ok(()) -} - -/// Exposes the archiving configuration that is otherwise not observable from -/// outside the canister, with the defaults of the optional `ArchiveOptions` -/// fields already applied. -/// -/// `trigger_threshold`, `num_blocks_to_archive` and `max_message_size_bytes` -/// govern the ledger's own behaviour and take effect immediately. The remaining -/// settings are only used when the ledger spawns a *new* archive: an archive -/// that already exists keeps the values it was installed with, and reports them -/// through its own metrics. -fn encode_archive_config_metrics( - w: &mut ic_metrics_encoder::MetricsEncoder>, - archive: &Archive, -) -> std::io::Result<()> -where - Rt: Runtime, - Wasm: ArchiveCanisterWasm, -{ - w.encode_gauge( - "ledger_archive_trigger_threshold", - archive.trigger_threshold as f64, - "The number of blocks which, when exceeded, triggers archiving.", - )?; - w.encode_gauge( - "ledger_archive_num_blocks_to_archive", - archive.num_blocks_to_archive as f64, - "The number of blocks archived when the trigger threshold is exceeded.", - )?; - w.encode_gauge( - "ledger_archive_node_max_memory_size_bytes", - archive.node_max_memory_size_bytes.min(ARCHIVE_MEMORY_LIMIT) as f64, - "Maximum number of bytes an archive spawned from now on may store. Existing \ - archives keep the cap they were created with, reported by their own \ - archive_max_memory_size_bytes metric.", - )?; - w.encode_gauge( - "ledger_archive_max_message_size_bytes", - archive.max_message_size_bytes as f64, - "Archive option limiting the size in bytes of a message sent to an archive. \ - The size actually used is the smaller of this and the ledger's own \ - ledger_max_message_size_bytes.", - )?; - w.encode_gauge( - "ledger_archive_cycles_for_archive_creation", - archive.cycles_for_archive_creation as f64, - "Cycles that will be attached to the call creating the next archive canister.", - )?; - w.encode_gauge( - "ledger_archive_max_transactions_per_response", - archive - .max_transactions_per_response - .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE) as f64, - "Maximum number of transactions an archive spawned from now on will return \ - per response. Existing archives keep the limit they were created with, \ - reported by their own archive_max_transactions_per_response metric.", - )?; - Ok(()) -} - fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::io::Result<()> { w.encode_gauge( "ledger_stable_memory_pages", @@ -490,7 +407,21 @@ fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::i "Total number of archives.", )?; if let Some(archive) = archive_guard.as_ref() { - encode_archive_config_metrics(w, archive)?; + encode_archive_config_metrics( + w, + archive, + archive.node_max_memory_size_bytes.min(ARCHIVE_MEMORY_LIMIT), + )?; + w.encode_gauge( + "ledger_archive_max_transactions_per_response", + archive + .max_transactions_per_response + .unwrap_or(DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE) + as f64, + "Maximum number of transactions an archive spawned from now on will \ + return per response. Existing archives keep the limit they were created \ + with, reported by their own archive_max_transactions_per_response metric.", + )?; } } Err(err) => Err(std::io::Error::other(format!( From fa41359d0b486411b1942739222e3152ac7c282f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Wed, 2 Sep 2026 15:11:06 +0200 Subject: [PATCH 7/7] Comment cleanup --- rs/ledger_suite/icrc1/src/archive_limits.rs | 18 +----------------- rs/ledger_suite/tests/sm-tests/src/lib.rs | 7 ------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/rs/ledger_suite/icrc1/src/archive_limits.rs b/rs/ledger_suite/icrc1/src/archive_limits.rs index 360928a2d476..4f8052d6d2a2 100644 --- a/rs/ledger_suite/icrc1/src/archive_limits.rs +++ b/rs/ledger_suite/icrc1/src/archive_limits.rs @@ -1,26 +1,10 @@ //! Limits the ICRC archive canister applies to itself. -//! -//! These live here, rather than privately in the archive canister, because the -//! ledger has to report the values that will actually take effect: it decides -//! what to pass to a new archive, but the archive is what enforces them. Keeping -//! them in a crate both canisters depend on means the two cannot disagree. -//! -//! They are deliberately not in `ic_ledger_canister_core`, which the ICP ledger -//! also uses: neither limit applies there. The ICP archive has no -//! `max_transactions_per_response` setting at all, and does not bound the memory -//! cap it is given. /// The default maximum number of transactions returned by an ICRC archive's /// `get_transactions` endpoint, applied when `max_transactions_per_response` is /// not set. -/// -/// This bears only on archives the ledger spawns from now on. An archive that -/// already exists was installed with whatever value was configured, and with -/// whatever default the archive Wasm of the day applied, and keeps using it. pub const DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE: u64 = 2000; /// The hard upper bound an ICRC archive places on the number of bytes it will -/// use to store encoded blocks. Its `init` both defaults to and clamps to this -/// value, so a larger `node_max_memory_size_bytes` configured on the ledger has -/// no effect: the archive will still store at most this much. +/// use to store encoded blocks. pub const ARCHIVE_MEMORY_LIMIT: u64 = 3 * 1024 * 1024 * 1024; diff --git a/rs/ledger_suite/tests/sm-tests/src/lib.rs b/rs/ledger_suite/tests/sm-tests/src/lib.rs index 6cf34840b421..26988c4dd631 100644 --- a/rs/ledger_suite/tests/sm-tests/src/lib.rs +++ b/rs/ledger_suite/tests/sm-tests/src/lib.rs @@ -1519,8 +1519,6 @@ pub fn test_archive_and_dedup_config_metrics( assert_eq!(metric("ledger_archive_node_max_memory_size_bytes"), 5678); assert_eq!(metric("ledger_archive_max_transactions_per_response"), 42); - // An ICRC archive clamps its memory cap, so configuring more than it accepts - // must not be reported as if the archive would honour it. let above_cap = LedgerArgument::Upgrade(Some(UpgradeArgs { change_archive_options: Some(ChangeArchiveOptions { node_max_memory_size_bytes: Some(ARCHIVE_MEMORY_LIMIT * 2), @@ -1542,9 +1540,6 @@ pub fn test_archive_and_dedup_config_metrics( ); } -/// The ledger's archive settings describe archives it will spawn from now on. -/// An archive that already exists keeps what it was installed with, so it has to -/// report its own effective settings. pub fn test_archive_reports_its_own_config_metrics( ledger_wasm: Vec, encode_init_args: fn(InitArgs) -> T, @@ -1577,8 +1572,6 @@ pub fn test_archive_reports_its_own_config_metrics( DEFAULT_MAX_TRANSACTIONS_PER_RESPONSE ); - // Changing the ledger's settings must not change what the existing archive - // enforces. let upgrade_args = LedgerArgument::Upgrade(Some(UpgradeArgs { change_archive_options: Some(ChangeArchiveOptions { node_max_memory_size_bytes: Some(ledger_cap + 4096),