Skip to content

Commit 215a697

Browse files
maciejdfinitymbjorkqvistmraszyk
authored
feat: ICP-ledger: FI-1440: Implement V4 for ICP ledger - migrate balances to stable structures (dfinity#3314)
This version of the ledger migrates the balances stored by the ledger from heap to stable memory stable structures. This allows the ledger to store more data (heap memory is limited to 4GB) and reduces the number of instructions used during the upgrades - there is no need to serialize and deserialize the balances stored in heap memory. Since we are no longer limited by the heap size, the logic for trimming balances and related init/upgrade arguments (`maximum_number_of_accounts`, `accounts_overflow_trim_quantity`) were removed. The migration to stable structures is performed during the upgrade and, if 250B instruction limit is reached, using timer invocations after the upgrade. If timer invocations are used by the migration, the ledger is unavailable for transfers and approvals until migration is finished. The migration status can be checked by calling the `is_ledger_ready` endpoint. After migration is finished, it is no longer possible to downgrade the ledger to the previous version . --------- Co-authored-by: Mathias Björkqvist <[email protected]> Co-authored-by: mraszyk <[email protected]>
1 parent c05b185 commit 215a697

File tree

26 files changed

+284
-868
lines changed

26 files changed

+284
-868
lines changed

Cargo.lock

Lines changed: 0 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,6 @@ members = [
216216
"rs/nns/handlers/root/interface",
217217
"rs/nns/identity",
218218
"rs/nns/init",
219-
"rs/nns/inspector",
220219
"rs/nns/integration_tests",
221220
"rs/nns/nns-ui",
222221
"rs/nns/test_utils",

WORKSPACE.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ canisters(
1717
"registry": "registry-canister.wasm.gz",
1818
"governance": "governance-canister.wasm.gz",
1919
"ledger": "ledger-canister_notify-method.wasm.gz",
20+
"ledger_v1": "ledger-canister_notify-method.wasm.gz",
21+
"ledger_v2": "ledger-canister_notify-method.wasm.gz",
2022
"archive": "ledger-archive-node-canister.wasm.gz",
2123
"index": "ic-icp-index-canister.wasm.gz",
2224
"root": "root-canister.wasm.gz",
@@ -51,6 +53,8 @@ canisters(
5153
"registry": "mainnet_nns_registry_canister",
5254
"governance": "mainnet_nns_governance_canister",
5355
"ledger": "mainnet_icp_ledger_canister",
56+
"ledger_v1": "mainnet_icp_ledger_canister-v1",
57+
"ledger_v2": "mainnet_icp_ledger_canister-v2",
5458
"archive": "mainnet_icp_ledger-archive-node-canister",
5559
"index": "mainnet_icp_index_canister",
5660
"root": "mainnet_nns_root-canister",

mainnet-canister-revisions.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@
7979
"rev": "7c6309cb5bec7ab28ed657ac7672af08a59fc1ba",
8080
"sha256": "a9ed1cb9dda555e0fc1038825eb7b3a6b366f17aa4b88575184c7537e864e551"
8181
},
82+
"ledger_v1": {
83+
"rev": "6dcfafb491092704d374317d9a72a7ad2475d7c9",
84+
"sha256": "4fe38a91a3130e9d8b39e3413ae3b3f46c40d3fbd507df1b6092f962d970a7ea"
85+
},
86+
"ledger_v2": {
87+
"rev": "dac2f36f96d7549d82fa8e3c714979255ce57afd",
88+
"sha256": "50c05fd687883fe788c0bb91996de358d8f856ba56088c6ff47767ea853001d7"
89+
},
8290
"lifeline": {
8391
"rev": "b5192581ccd35b67fe5a1f795ead9cbcd25956d6",
8492
"sha256": "8c8eb285de53ca5609abd7dc41ba3ec8eeb67708b81469311fd670e6738d7d0a"

publish/canisters/BUILD.bazel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ CANISTERS_MAX_SIZE_COMPRESSED_E5_BYTES = {
6565
"ic-icrc1-ledger-u256.wasm.gz": "7",
6666
# Size when constraint addded: 841_234 bytes
6767
"ledger-canister.wasm.gz": "9",
68-
# Size when constraint addded: 847_186 bytes
69-
"ledger-canister_notify-method.wasm.gz": "9",
68+
# Size when constraint addded: 906_940 bytes
69+
"ledger-canister_notify-method.wasm.gz": "10",
7070

7171
# -- XC team --
7272
# Size when constraint addded: 447_593 bytes

rs/ledger_suite/common/ledger_canister_core/src/ledger.rs

Lines changed: 1 addition & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,18 @@ use ic_canister_log::{log, Sink};
44
use ic_ledger_core::approvals::{
55
AllowanceTable, AllowancesData, ApproveError, InsufficientAllowance,
66
};
7-
use ic_ledger_core::tokens::Zero;
87
use serde::{Deserialize, Serialize};
98
use std::collections::{BTreeMap, VecDeque};
109
use std::ops::Range;
1110
use std::time::Duration;
1211

1312
use crate::archive::{ArchivingGuardError, FailedToArchiveBlocks, LedgerArchivingGuard};
14-
use ic_ledger_core::balances::{BalanceError, Balances, BalancesStore, InspectableBalancesStore};
13+
use ic_ledger_core::balances::{BalanceError, Balances, BalancesStore};
1514
use ic_ledger_core::block::{BlockIndex, BlockType, EncodedBlock, FeeCollector};
1615
use ic_ledger_core::timestamp::TimeStamp;
1716
use ic_ledger_core::tokens::TokensType;
1817
use ic_ledger_hash_of::HashOf;
1918

20-
/// The memo to use for balances burned and approvals reset to 0 during trimming
21-
const TRIMMED_MEMO: u64 = u64::MAX;
22-
2319
#[derive(Debug, Deserialize, Serialize)]
2420
pub struct TransactionInfo<TransactionType> {
2521
pub block_timestamp: TimeStamp,
@@ -158,13 +154,6 @@ pub trait LedgerData: LedgerContext {
158154
/// The maximum number of transactions that we attempt to purge in one go.
159155
fn max_transactions_to_purge(&self) -> usize;
160156

161-
/// The maximum size of the balances map.
162-
fn max_number_of_accounts(&self) -> usize;
163-
164-
/// How many accounts with lowest balances to purge when the number of accounts exceeds
165-
/// [LedgerData::max_number_of_accounts].
166-
fn accounts_overflow_trim_quantity(&self) -> usize;
167-
168157
// Token configuration
169158

170159
/// Token name (e.g., Bitcoin).
@@ -208,30 +197,12 @@ pub enum TransferError<Tokens> {
208197
const APPROVE_PRUNE_LIMIT: usize = 100;
209198

210199
/// Adds a new block with the specified transaction to the ledger.
211-
/// Trim balances if necessary.
212200
pub fn apply_transaction<L>(
213201
ledger: &mut L,
214202
transaction: L::Transaction,
215203
now: TimeStamp,
216204
effective_fee: L::Tokens,
217205
) -> Result<(BlockIndex, HashOf<EncodedBlock>), TransferError<L::Tokens>>
218-
where
219-
L: LedgerData,
220-
L::BalancesStore: InspectableBalancesStore,
221-
{
222-
let result = apply_transaction_no_trimming(ledger, transaction, now, effective_fee);
223-
trim_balances(ledger, now);
224-
result
225-
}
226-
227-
/// Adds a new block with the specified transaction to the ledger.
228-
/// Do not perform any balance trimming.
229-
pub fn apply_transaction_no_trimming<L>(
230-
ledger: &mut L,
231-
transaction: L::Transaction,
232-
now: TimeStamp,
233-
effective_fee: L::Tokens,
234-
) -> Result<(BlockIndex, HashOf<EncodedBlock>), TransferError<L::Tokens>>
235206
where
236207
L: LedgerData,
237208
{
@@ -322,44 +293,6 @@ where
322293
Ok((height, ledger.blockchain().last_hash.unwrap()))
323294
}
324295

325-
/// Trim balances. Can be used e.g. if the ledger is low on heap memory.
326-
fn trim_balances<L>(ledger: &mut L, now: TimeStamp)
327-
where
328-
L: LedgerData,
329-
L::BalancesStore: InspectableBalancesStore,
330-
{
331-
let effective_max_number_of_accounts =
332-
ledger.max_number_of_accounts() + ledger.accounts_overflow_trim_quantity() - 1;
333-
334-
let to_trim = if ledger.balances().store.len() > effective_max_number_of_accounts {
335-
select_accounts_to_trim(ledger)
336-
} else {
337-
vec![]
338-
};
339-
340-
for (balance, account) in to_trim {
341-
let burn_tx = L::Transaction::burn(account, None, balance, Some(now), Some(TRIMMED_MEMO));
342-
343-
burn_tx
344-
.apply(ledger, now, L::Tokens::zero())
345-
.expect("failed to burn funds that must have existed");
346-
347-
let parent_hash = ledger.blockchain().last_hash;
348-
let fee_collector = ledger.fee_collector().cloned();
349-
350-
ledger
351-
.blockchain_mut()
352-
.add_block(L::Block::from_transaction(
353-
parent_hash,
354-
burn_tx,
355-
now,
356-
L::Tokens::zero(),
357-
fee_collector,
358-
))
359-
.unwrap();
360-
}
361-
}
362-
363296
/// Finds the archive canister that contains the block with the specified height.
364297
pub fn find_block_in_archive<L: LedgerData>(ledger: &L, block_height: u64) -> Option<CanisterId> {
365298
let index = ledger
@@ -453,39 +386,6 @@ pub fn purge_old_transactions<L: LedgerData>(ledger: &mut L, now: TimeStamp) ->
453386
num_tx_purged
454387
}
455388

456-
// Find the specified number of accounts with lowest balances so that their
457-
// balances can be reclaimed.
458-
fn select_accounts_to_trim<L>(ledger: &L) -> Vec<(L::Tokens, L::AccountId)>
459-
where
460-
L: LedgerData,
461-
L::BalancesStore: InspectableBalancesStore<Tokens = L::Tokens>,
462-
L::Tokens: TokensType,
463-
{
464-
let mut to_trim: std::collections::BinaryHeap<(L::Tokens, L::AccountId)> =
465-
std::collections::BinaryHeap::new();
466-
467-
let num_accounts = ledger.accounts_overflow_trim_quantity();
468-
let mut iter = ledger.balances().store.iter();
469-
470-
// Accumulate up to `trim_quantity` accounts
471-
for (account, balance) in iter.by_ref().take(num_accounts) {
472-
to_trim.push((balance.clone(), account.clone()));
473-
}
474-
475-
for (account, balance) in iter {
476-
// If any account's balance is lower than the maximum in our set,
477-
// include that account, and remove the current maximum
478-
if let Some((greatest_balance, _)) = to_trim.peek() {
479-
if balance < greatest_balance {
480-
to_trim.push((balance.clone(), account.clone()));
481-
to_trim.pop();
482-
}
483-
}
484-
}
485-
486-
to_trim.into_vec()
487-
}
488-
489389
/// Asynchronously archives a suffix of the locally available blockchain.
490390
///
491391
/// NOTE: only one archiving task can run at each point in time.

rs/ledger_suite/common/ledger_core/src/balances.rs

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,6 @@ pub trait BalancesStore {
1919
F: FnMut(Option<&Self::Tokens>) -> Result<Self::Tokens, E>;
2020
}
2121

22-
#[allow(clippy::len_without_is_empty)]
23-
pub trait InspectableBalancesStore: BalancesStore {
24-
fn iter(&self) -> Box<dyn Iterator<Item = (&Self::AccountId, &Self::Tokens)> + '_>;
25-
26-
fn len(&self) -> usize;
27-
}
28-
2922
impl<AccountId, Tokens> BalancesStore for BTreeMap<AccountId, Tokens>
3023
where
3124
AccountId: Eq + Clone + std::cmp::Ord,
@@ -63,20 +56,6 @@ where
6356
}
6457
}
6558

66-
impl<AccountId, Tokens> InspectableBalancesStore for BTreeMap<AccountId, Tokens>
67-
where
68-
AccountId: Eq + Clone + std::cmp::Ord,
69-
Tokens: TokensType,
70-
{
71-
fn len(&self) -> usize {
72-
self.len()
73-
}
74-
75-
fn iter(&self) -> Box<dyn Iterator<Item = (&Self::AccountId, &Self::Tokens)> + '_> {
76-
Box::new(self.iter())
77-
}
78-
}
79-
8059
/// An error returned by `Balances` if the debit operation fails.
8160
#[derive(Debug)]
8261
pub enum BalanceError<Tokens> {

rs/ledger_suite/common/ledger_core/src/tokens.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
use candid::{CandidType, Nat};
2+
use ic_stable_structures::{storable::Bound, Storable};
23
use minicbor::{Decode, Encode};
34
use num_traits::{Bounded, ToPrimitive};
45
use serde::{de::DeserializeOwned, Deserialize, Serialize};
6+
use std::borrow::Cow;
57
use std::fmt;
68
use std::fmt::Debug;
79

10+
#[cfg(test)]
11+
mod tests;
12+
813
/// Performs addition that returns `None` instead of wrapping around on
914
/// overflow.
1015
///
@@ -302,3 +307,20 @@ impl From<Tokens> for Nat {
302307
Nat::from(value.e8s)
303308
}
304309
}
310+
311+
impl Storable for Tokens {
312+
fn to_bytes(&self) -> Cow<[u8]> {
313+
Cow::Owned(self.e8s.to_le_bytes().to_vec())
314+
}
315+
316+
fn from_bytes(bytes: Cow<[u8]>) -> Self {
317+
Self {
318+
e8s: u64::from_le_bytes(bytes.into_owned().as_slice().try_into().unwrap()),
319+
}
320+
}
321+
322+
const BOUND: Bound = Bound::Bounded {
323+
max_size: 8,
324+
is_fixed_size: true,
325+
};
326+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use crate::tokens::Tokens;
2+
use ic_stable_structures::Storable;
3+
use proptest::prelude::{any, prop_assert_eq, proptest};
4+
5+
#[test]
6+
fn tokens_serialization() {
7+
proptest!(|(e8s in any::<u64>())| {
8+
let tokens = Tokens { e8s };
9+
let new_tokens = Tokens::from_bytes(tokens.to_bytes());
10+
prop_assert_eq!(new_tokens, tokens);
11+
})
12+
}

rs/ledger_suite/icp/ledger.did

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,6 @@ type InitArgs = record {
302302
token_symbol: opt text;
303303
token_name: opt text;
304304
feature_flags : opt FeatureFlags;
305-
maximum_number_of_accounts : opt nat64;
306-
accounts_overflow_trim_quantity: opt nat64;
307305
};
308306

309307
type Icrc1BlockIndex = nat;

0 commit comments

Comments
 (0)