Skip to content

Commit c3f0331

Browse files
authored
feat(ICRC_Ledger): FI-1657: Export total volume counter metric for ICRC ledger (#4166)
Export a `total_volume` counter metric for the ICRC ledger.
1 parent 389a25f commit c3f0331

File tree

3 files changed

+208
-6
lines changed

3 files changed

+208
-6
lines changed

rs/ledger_suite/icrc1/ledger/src/main.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ thread_local! {
8484
static PRE_UPGRADE_INSTRUCTIONS_CONSUMED: RefCell<u64> = const { RefCell::new(0) };
8585
static POST_UPGRADE_INSTRUCTIONS_CONSUMED: RefCell<u64> = const { RefCell::new(0) };
8686
static STABLE_UPGRADE_MIGRATION_STEPS: RefCell<u64> = const { RefCell::new(0) };
87+
static TOTAL_VOLUME: RefCell<f64> = const { RefCell::new(0f64) };
88+
static TOTAL_VOLUME_DENOMINATOR: RefCell<f64> = const { RefCell::new(1f64) };
89+
static TOTAL_VOLUME_FEE_IN_DECIMALS: RefCell<f64> = const { RefCell::new(0f64) };
8790
}
8891

8992
declare_log_buffer!(name = LOG, capacity = 1000);
@@ -126,7 +129,8 @@ fn init(args: LedgerArgument) {
126129

127130
fn init_state(init_args: InitArgs) {
128131
let now = TimeStamp::from_nanos_since_unix_epoch(ic_cdk::api::time());
129-
LEDGER.with(|cell| *cell.borrow_mut() = Some(Ledger::from_init_args(&LOG, init_args, now)))
132+
LEDGER.with(|cell| *cell.borrow_mut() = Some(Ledger::from_init_args(&LOG, init_args, now)));
133+
initialize_total_volume();
130134
}
131135

132136
// We use 8MiB buffer
@@ -239,6 +243,8 @@ fn post_upgrade(args: Option<LedgerArgument>) {
239243

240244
PRE_UPGRADE_INSTRUCTIONS_CONSUMED.with(|n| *n.borrow_mut() = pre_upgrade_instructions_consumed);
241245

246+
initialize_total_volume();
247+
242248
if upgrade_from_version < 3 {
243249
set_ledger_state(LedgerState::Migrating(LedgerField::Blocks));
244250
log_message(format!("Upgrading from version {upgrade_from_version} which does not store blocks in stable structures, clearing stable blocks data.").as_str());
@@ -272,6 +278,15 @@ fn post_upgrade(args: Option<LedgerArgument>) {
272278
POST_UPGRADE_INSTRUCTIONS_CONSUMED.with(|n| *n.borrow_mut() = instructions_consumed);
273279
}
274280

281+
fn initialize_total_volume() {
282+
let denominator = 10f64.powf(Access::with_ledger(|ledger| ledger.decimals()) as f64);
283+
let fee = Access::with_ledger(|ledger| ledger.transfer_fee());
284+
TOTAL_VOLUME_DENOMINATOR.with(|n| *n.borrow_mut() = denominator);
285+
if fee != Tokens::ZERO {
286+
TOTAL_VOLUME_FEE_IN_DECIMALS.with(|n| *n.borrow_mut() = tokens_to_f64(fee) / denominator);
287+
}
288+
}
289+
275290
fn migrate_next_part(instruction_limit: u64) {
276291
let instructions_migration_start = instruction_counter();
277292
STABLE_UPGRADE_MIGRATION_STEPS.with(|n| *n.borrow_mut() += 1);
@@ -390,6 +405,11 @@ fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder<Vec<u8>>) -> std::i
390405
stable_upgrade_migration_steps as f64,
391406
"Number of steps used to migrate data to stable structures.",
392407
)?;
408+
w.encode_counter(
409+
"total_volume",
410+
TOTAL_VOLUME.with(|n| *n.borrow()),
411+
"Total volume of ledger transactions.",
412+
)?;
393413

394414
Access::with_ledger(|ledger| {
395415
w.encode_gauge(
@@ -475,6 +495,50 @@ fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder<Vec<u8>>) -> std::i
475495
})
476496
}
477497

498+
/// Update the total volume of token transactions. Since the total volume counter is an `f64`, it
499+
/// can handle large amounts, but the accuracy may suffer. Only the rate of increase of the counter
500+
/// should be used, since the total amount will be reset to zero each time the canister is upgraded.
501+
fn update_total_volume(amount: Tokens, with_fee: bool) {
502+
let mut total_volume = TOTAL_VOLUME.with(|n| *n.borrow());
503+
let denominator = TOTAL_VOLUME_DENOMINATOR.with(|n| *n.borrow());
504+
if amount != Tokens::ZERO {
505+
let amount = tokens_to_f64(amount) / denominator;
506+
total_volume = f64_saturating_add(total_volume, amount);
507+
}
508+
if with_fee {
509+
total_volume = f64_saturating_add(
510+
total_volume,
511+
TOTAL_VOLUME_FEE_IN_DECIMALS.with(|n| *n.borrow()),
512+
);
513+
}
514+
TOTAL_VOLUME.with(|n| *n.borrow_mut() = total_volume);
515+
}
516+
517+
fn f64_saturating_add(a: f64, b: f64) -> f64 {
518+
let sum = a + b;
519+
520+
if sum.is_infinite() && sum.is_sign_positive() {
521+
// If positive infinity, clamp to f64::MAX
522+
f64::MAX
523+
} else if sum.is_infinite() && sum.is_sign_negative() {
524+
// If negative infinity, clamp to f64::MIN
525+
f64::MIN
526+
} else {
527+
// Otherwise, return the regular sum
528+
sum
529+
}
530+
}
531+
532+
#[cfg(not(feature = "u256-tokens"))]
533+
fn tokens_to_f64(tokens: Tokens) -> f64 {
534+
tokens.to_u64() as f64
535+
}
536+
537+
#[cfg(feature = "u256-tokens")]
538+
fn tokens_to_f64(tokens: Tokens) -> f64 {
539+
tokens.to_u256().as_f64()
540+
}
541+
478542
#[query(hidden = true, decoding_quota = 10000)]
479543
fn http_request(req: HttpRequest) -> HttpResponse {
480544
if req.path() == "/metrics" {
@@ -685,6 +749,7 @@ fn execute_transfer_not_async(
685749
};
686750

687751
let (block_idx, _) = apply_transaction(ledger, tx, now, effective_fee)?;
752+
update_total_volume(amount, effective_fee != Tokens::zero());
688753
Ok(block_idx)
689754
})
690755
}
@@ -900,6 +965,8 @@ fn icrc2_approve_not_async(caller: Principal, arg: ApproveArgs) -> Result<u64, A
900965
Ok(block_idx)
901966
})?;
902967

968+
update_total_volume(Tokens::zero(), true);
969+
903970
Ok(block_idx)
904971
}
905972

rs/ledger_suite/icrc1/ledger/tests/tests.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ fn encode_init_args(args: ic_ledger_suite_state_machine_tests::InitArgs) -> Ledg
209209
minting_account: MINTER,
210210
fee_collector_account: args.fee_collector_account,
211211
initial_balances: args.initial_balances,
212-
transfer_fee: FEE.into(),
212+
transfer_fee: args.transfer_fee,
213213
token_name: TOKEN_NAME.to_string(),
214214
decimals: Some(DECIMAL_PLACES),
215215
token_symbol: TOKEN_SYMBOL.to_string(),
@@ -872,6 +872,14 @@ mod metrics {
872872
encode_upgrade_args,
873873
);
874874
}
875+
876+
#[test]
877+
fn should_compute_and_export_total_volume_metric() {
878+
ic_ledger_suite_state_machine_tests::metrics::should_compute_and_export_total_volume_metric(
879+
ledger_wasm(),
880+
encode_init_args,
881+
);
882+
}
875883
}
876884

877885
// Validate upgrade of the Ledger from previous versions

rs/ledger_suite/tests/sm-tests/src/metrics.rs

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
use crate::{setup, transfer, InitArgs, ARCHIVE_TRIGGER_THRESHOLD, MINTER, NUM_BLOCKS_TO_ARCHIVE};
1+
use crate::{
2+
default_approve_args, init_args, send_approval, setup, transfer, InitArgs,
3+
ARCHIVE_TRIGGER_THRESHOLD, DECIMAL_PLACES, MINTER, NUM_BLOCKS_TO_ARCHIVE,
4+
};
25
use candid::{CandidType, Decode, Encode, Principal};
36
use ic_base_types::{CanisterId, PrincipalId};
47
use ic_canisters_http_types::{HttpRequest, HttpResponse};
58
use ic_state_machine_tests::StateMachine;
69
use ic_types::ingress::WasmResult;
10+
use icrc_ledger_types::icrc2::approve::ApproveArgs;
11+
use std::str::FromStr;
712

813
pub enum LedgerSuiteType {
914
ICP,
@@ -157,6 +162,127 @@ pub fn assert_ledger_upgrade_instructions_consumed_metric_set<T, U>(
157162
);
158163
}
159164

165+
pub fn should_compute_and_export_total_volume_metric<T>(
166+
ledger_wasm: Vec<u8>,
167+
encode_init_args: fn(InitArgs) -> T,
168+
) where
169+
T: CandidType,
170+
{
171+
const TOTAL_VOLUME_METRIC: &str = "total_volume";
172+
let mut expected_total = 0f64;
173+
174+
let initial_balances = vec![(
175+
PrincipalId::new_user_test_id(1).0.into(),
176+
u64::MAX - 10_000_000,
177+
)];
178+
let env = StateMachine::new();
179+
180+
let transfer_fee = 10f64.powf(DECIMAL_PLACES as f64 - 1f64) as u64;
181+
println!("transfer_fee: {}", transfer_fee);
182+
let args = InitArgs {
183+
transfer_fee: transfer_fee.into(),
184+
..init_args(initial_balances)
185+
};
186+
let args = Encode!(&encode_init_args(args)).unwrap();
187+
let canister_id = env.install_canister(ledger_wasm, args, None).unwrap();
188+
189+
let denominator = 10f64.powf(DECIMAL_PLACES as f64);
190+
191+
let mut increase_expected_total_volume_and_assert = |amount: u64| {
192+
expected_total += amount as f64 / denominator;
193+
assert_eq!(
194+
format!("{:.0}", expected_total),
195+
format!(
196+
"{:.0}",
197+
parse_metric(&env, canister_id, TOTAL_VOLUME_METRIC)
198+
)
199+
);
200+
};
201+
202+
// Verify the metric returns 0 when no transactions have occurred
203+
assert_eq!(0, parse_metric(&env, canister_id, TOTAL_VOLUME_METRIC));
204+
205+
// Perform a bunch of small transfers to verify that the computation of decimals is correct,
206+
// and so that the total fee exceeds 1.0.
207+
let num_operations = denominator as u64 / transfer_fee;
208+
println!("performing {} transfers", num_operations);
209+
for _ in 0..num_operations {
210+
transfer(
211+
&env,
212+
canister_id,
213+
PrincipalId::new_user_test_id(1).0,
214+
PrincipalId::new_user_test_id(2).0,
215+
transfer_fee,
216+
)
217+
.expect("transfer failed");
218+
}
219+
increase_expected_total_volume_and_assert(2 * num_operations * transfer_fee);
220+
221+
// Verify total volume accounting handles minting correctly (no fee).
222+
for _ in 0..num_operations {
223+
transfer(
224+
&env,
225+
canister_id,
226+
MINTER,
227+
PrincipalId::new_user_test_id(1).0,
228+
transfer_fee,
229+
)
230+
.expect("mint failed");
231+
}
232+
increase_expected_total_volume_and_assert(num_operations * transfer_fee);
233+
234+
// Verify total volume accounting handles burning correctly (no fee).
235+
for _ in 0..num_operations {
236+
transfer(
237+
&env,
238+
canister_id,
239+
PrincipalId::new_user_test_id(1).0,
240+
MINTER,
241+
transfer_fee,
242+
)
243+
.expect("burn failed");
244+
}
245+
increase_expected_total_volume_and_assert(num_operations * transfer_fee);
246+
247+
// Verify total volume accounting handles approvals correctly (no amount).
248+
let approve_args = ApproveArgs {
249+
fee: Some(transfer_fee.into()),
250+
..default_approve_args(PrincipalId::new_user_test_id(1).0, 1_000_000_000)
251+
};
252+
for _ in 0..num_operations {
253+
send_approval(
254+
&env,
255+
canister_id,
256+
PrincipalId::new_user_test_id(2).0,
257+
&approve_args,
258+
)
259+
.expect("approval failed");
260+
}
261+
increase_expected_total_volume_and_assert(num_operations * transfer_fee);
262+
263+
// Perform some larger transfers to verify a total volume larger than u64::MAX is handled correctly.
264+
transfer(
265+
&env,
266+
canister_id,
267+
PrincipalId::new_user_test_id(1).0,
268+
PrincipalId::new_user_test_id(2).0,
269+
u64::MAX - 1_000_000_000,
270+
)
271+
.expect("transfer failed");
272+
increase_expected_total_volume_and_assert(u64::MAX - 1_000_000_000 + transfer_fee);
273+
274+
transfer(
275+
&env,
276+
canister_id,
277+
PrincipalId::new_user_test_id(2).0,
278+
PrincipalId::new_user_test_id(1).0,
279+
u64::MAX - 10_000_000_000,
280+
)
281+
.expect("transfer failed");
282+
283+
increase_expected_total_volume_and_assert(u64::MAX - 10_000_000_000 + transfer_fee);
284+
}
285+
160286
fn assert_existence_of_metric(env: &StateMachine, canister_id: CanisterId, metric: &str) {
161287
let metrics = retrieve_metrics(env, canister_id);
162288
assert!(
@@ -180,9 +306,10 @@ pub fn parse_metric(env: &StateMachine, canister_id: CanisterId, metric: &str) -
180306
let value_str = *tokens
181307
.get(1)
182308
.unwrap_or_else(|| panic!("metric line '{}' should have at least two tokens", line));
183-
return value_str
184-
.parse()
185-
.unwrap_or_else(|err| panic!("metric value is not an integer: {} ({})", line, err));
309+
let u64_value = f64::from_str(value_str)
310+
.unwrap_or_else(|err| panic!("metric value is not an number: {} ({})", line, err))
311+
.round() as u64;
312+
return u64_value;
186313
}
187314
panic!("metric '{}' not found in metrics: {:?}", metric, metrics);
188315
}

0 commit comments

Comments
 (0)