diff --git a/packages/icrc-ledger-types/src/icrc106/errors.rs b/packages/icrc-ledger-types/src/icrc106/errors.rs new file mode 100644 index 000000000000..81360d1cdf66 --- /dev/null +++ b/packages/icrc-ledger-types/src/icrc106/errors.rs @@ -0,0 +1,11 @@ +use candid::{CandidType, Deserialize, Nat}; +use serde::Serialize; + +#[derive(Debug, CandidType, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum Icrc106Error { + IndexPrincipalNotSet, + GenericError { + error_code: Nat, + description: String, + }, +} diff --git a/packages/icrc-ledger-types/src/icrc106/mod.rs b/packages/icrc-ledger-types/src/icrc106/mod.rs new file mode 100644 index 000000000000..629e98fbf874 --- /dev/null +++ b/packages/icrc-ledger-types/src/icrc106/mod.rs @@ -0,0 +1 @@ +pub mod errors; diff --git a/packages/icrc-ledger-types/src/lib.rs b/packages/icrc-ledger-types/src/lib.rs index 2208b82f7f86..21ee656e39c2 100644 --- a/packages/icrc-ledger-types/src/lib.rs +++ b/packages/icrc-ledger-types/src/lib.rs @@ -3,6 +3,7 @@ pub mod icrc; pub mod icrc1; pub mod icrc103; +pub mod icrc106; pub mod icrc2; pub mod icrc21; pub mod icrc3; diff --git a/publish/canisters/BUILD.bazel b/publish/canisters/BUILD.bazel index 767d97231642..e1bf0313e417 100644 --- a/publish/canisters/BUILD.bazel +++ b/publish/canisters/BUILD.bazel @@ -65,10 +65,11 @@ DEFAULT_CANISTERS_MAX_SIZE_E5_BYTES = 21 CANISTERS_MAX_SIZE_COMPRESSED_E5_BYTES = { # -- FI team -- - # The compressed version of these two canisters should be ~600kb, - # we are setting the check to 7 to leave some space for growth + # The compressed version of these two canisters should be ~700kb, + # we are setting the check to 8 to leave some space for growth # but enough to get an alert in case of a spike in size. - "ic-icrc1-ledger.wasm.gz": 7, + # The size is currently at 701737 bytes. + "ic-icrc1-ledger.wasm.gz": 8, # The size is currently at 704585 bytes. "ic-icrc1-ledger-u256.wasm.gz": 8, # Size when constraint addded: 841_234 bytes diff --git a/rs/ethereum/ledger-suite-orchestrator/src/scheduler/mod.rs b/rs/ethereum/ledger-suite-orchestrator/src/scheduler/mod.rs index af80736d5131..400d4e9763e5 100644 --- a/rs/ethereum/ledger-suite-orchestrator/src/scheduler/mod.rs +++ b/rs/ethereum/ledger-suite-orchestrator/src/scheduler/mod.rs @@ -834,6 +834,9 @@ async fn install_ledger_suite( let ledger_canister_id = create_canister_once::(&args.contract, runtime, cycles_for_ledger_creation) .await?; + let index_principal = + create_canister_once::(&args.contract, runtime, cycles_for_index_creation) + .await?; let more_controllers = read_state(|s| s.more_controller_ids().to_vec()) .into_iter() @@ -848,14 +851,12 @@ async fn install_ledger_suite( runtime.id().into(), more_controllers, cycles_for_archive_creation, + index_principal, )), runtime, ) .await?; - let _index_principal = - create_canister_once::(&args.contract, runtime, cycles_for_index_creation) - .await?; let index_arg = Some(IndexArg::Init(IndexInitArg { ledger_id: ledger_canister_id, retrieve_blocks_from_ledger_interval_seconds: None, @@ -898,6 +899,7 @@ fn icrc1_ledger_init_arg( archive_controller_id: PrincipalId, archive_more_controller_ids: Vec, cycles_for_archive_creation: Nat, + index_principal: Principal, ) -> LedgerInitArgs { use ic_icrc1_ledger::FeatureFlags as LedgerFeatureFlags; use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue as LedgerMetadataValue; @@ -932,6 +934,7 @@ fn icrc1_ledger_init_arg( ), max_memo_length: Some(MAX_MEMO_LENGTH), feature_flags: Some(ICRC2_FEATURE), + index_principal: Some(index_principal), } } diff --git a/rs/ethereum/ledger-suite-orchestrator/src/scheduler/tests.rs b/rs/ethereum/ledger-suite-orchestrator/src/scheduler/tests.rs index c155d2e8167d..b4a6ed769b3c 100644 --- a/rs/ethereum/ledger-suite-orchestrator/src/scheduler/tests.rs +++ b/rs/ethereum/ledger-suite-orchestrator/src/scheduler/tests.rs @@ -189,19 +189,15 @@ async fn should_not_retry_successful_operation_after_failing_one() { let mut runtime = MockCanisterRuntime::new(); runtime.expect_id().return_const(ORCHESTRATOR_PRINCIPAL); + let expected_error = CallError { + method: "create_canister".to_string(), + reason: Reason::OutOfCycles, + }; expect_create_canister_returning( &mut runtime, vec![ORCHESTRATOR_PRINCIPAL], - vec![Ok(LEDGER_PRINCIPAL)], + vec![Err(expected_error.clone())], ); - let expected_error = CallError { - method: "install_code".to_string(), - reason: Reason::OutOfCycles, - }; - runtime - .expect_install_code() - .times(1) - .return_const(Err(expected_error.clone())); let task = TaskExecution { task_type: Task::InstallLedgerSuite(usdc_install_args()), @@ -209,14 +205,12 @@ async fn should_not_retry_successful_operation_after_failing_one() { }; assert_eq!( task.execute(&runtime).await, - Err(TaskError::InstallCodeError(expected_error)) + Err(TaskError::CanisterCreationError(expected_error)) ); assert_eq!( read_state(|s| s.managed_canisters(&usdc_token_id()).cloned()), Some(Canisters { - ledger: Some(LedgerCanister::new(ManagedCanisterStatus::Created { - canister_id: LEDGER_PRINCIPAL - })), + ledger: None, index: None, archives: vec![], metadata: usdc_metadata(), @@ -229,13 +223,16 @@ async fn should_not_retry_successful_operation_after_failing_one() { method: "create_canister".to_string(), reason: Reason::OutOfCycles, }; - runtime.expect_install_code().times(1).return_const(Ok(())); expect_create_canister_returning( &mut runtime, vec![ORCHESTRATOR_PRINCIPAL], - vec![Err(expected_error.clone())], + vec![Ok(LEDGER_PRINCIPAL), Err(expected_error.clone())], ); + let task = TaskExecution { + task_type: Task::InstallLedgerSuite(usdc_install_args()), + execute_at_ns: 0, + }; assert_eq!( task.execute(&runtime).await, Err(TaskError::CanisterCreationError(expected_error)) @@ -243,9 +240,8 @@ async fn should_not_retry_successful_operation_after_failing_one() { assert_eq!( read_state(|s| s.managed_canisters(&usdc_token_id()).cloned()), Some(Canisters { - ledger: Some(LedgerCanister::new(ManagedCanisterStatus::Installed { - canister_id: LEDGER_PRINCIPAL, - installed_wasm_hash: read_ledger_wasm_hash(), + ledger: Some(LedgerCanister::new(ManagedCanisterStatus::Created { + canister_id: LEDGER_PRINCIPAL })), index: None, archives: vec![], @@ -269,6 +265,36 @@ async fn should_not_retry_successful_operation_after_failing_one() { .times(1) .return_const(Err(expected_error.clone())); + let task = TaskExecution { + task_type: Task::InstallLedgerSuite(usdc_install_args()), + execute_at_ns: 0, + }; + assert_eq!( + task.execute(&runtime).await, + Err(TaskError::InstallCodeError(expected_error)) + ); + assert_eq!( + read_state(|s| s.managed_canisters(&usdc_token_id()).cloned()), + Some(Canisters { + ledger: Some(LedgerCanister::new(ManagedCanisterStatus::Created { + canister_id: LEDGER_PRINCIPAL + })), + index: Some(IndexCanister::new(ManagedCanisterStatus::Created { + canister_id: INDEX_PRINCIPAL + })), + archives: vec![], + metadata: usdc_metadata(), + }) + ); + + runtime.checkpoint(); + runtime.expect_id().return_const(ORCHESTRATOR_PRINCIPAL); + let expected_error = CallError { + method: "install_code".to_string(), + reason: Reason::OutOfCycles, + }; + expect_install_code_returning(&mut runtime, vec![Ok(()), Err(expected_error.clone())]); + assert_eq!( task.execute(&runtime).await, Err(TaskError::InstallCodeError(expected_error)) @@ -325,7 +351,7 @@ async fn should_discard_add_erc20_task_when_ledger_wasm_not_found() { expect_create_canister_returning( &mut runtime, vec![ORCHESTRATOR_PRINCIPAL], - vec![Ok(LEDGER_PRINCIPAL)], + vec![Ok(LEDGER_PRINCIPAL), Ok(INDEX_PRINCIPAL)], ); assert_eq!( @@ -340,7 +366,9 @@ async fn should_discard_add_erc20_task_when_ledger_wasm_not_found() { ledger: Some(LedgerCanister::new(ManagedCanisterStatus::Created { canister_id: LEDGER_PRINCIPAL })), - index: None, + index: Some(IndexCanister::new(ManagedCanisterStatus::Created { + canister_id: INDEX_PRINCIPAL + })), archives: vec![], metadata: usdc_metadata(), }) @@ -1488,6 +1516,25 @@ fn expect_create_canister_returning( }); } +fn expect_install_code_returning( + runtime: &mut MockCanisterRuntime, + results: Vec>, +) { + assert!(!results.is_empty(), "must return at least one result"); + let mut install_code_call_counter = 0_usize; + runtime + .expect_install_code() + .times(results.len()) + .returning(move |_canister_id, _wasm, _args| { + if install_code_call_counter >= results.len() { + panic!("install_code called too many times!"); + } + let result = results[install_code_call_counter].clone(); + install_code_call_counter += 1; + result + }); +} + fn expect_call_canister_add_ckerc20_token( runtime: &mut MockCanisterRuntime, expected_canister_id: Principal, diff --git a/rs/ethereum/ledger-suite-orchestrator/tests/tests.rs b/rs/ethereum/ledger-suite-orchestrator/tests/tests.rs index 6e29dce46a89..210f88f3f5fd 100644 --- a/rs/ethereum/ledger-suite-orchestrator/tests/tests.rs +++ b/rs/ethereum/ledger-suite-orchestrator/tests/tests.rs @@ -36,12 +36,15 @@ fn should_spawn_ledger_with_correct_init_args() { }; let orchestrator = LedgerSuiteOrchestrator::default(); - orchestrator + let managed_canisters_assert = orchestrator .add_erc20_token(AddErc20Arg { contract: usdc_erc20_contract(), ledger_init_arg: realistic_usdc_ledger_init_arg, }) - .expect_new_ledger_and_index_canisters() + .expect_new_ledger_and_index_canisters(); + let index_id = managed_canisters_assert.canister_ids.index.unwrap(); + assert_eq!(index_id, "ryjl3-tyaaa-aaaaa-aaaba-cai".parse().unwrap()); + managed_canisters_assert .assert_ledger_icrc1_fee(2_000_000_000_000_u64) .assert_ledger_icrc1_decimals(6_u8) .assert_ledger_icrc1_name("USD Coin") @@ -84,6 +87,10 @@ fn should_spawn_ledger_with_correct_init_args() { "icrc103:max_take_value".to_string(), LedgerMetadataValue::from(500u64), ), + ( + "icrc106:index_principal".to_string(), + LedgerMetadataValue::from("ryjl3-tyaaa-aaaaa-aaaba-cai"), + ), ]); } diff --git a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs index 6a1bb3e35f94..9576d3ab4917 100644 --- a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs +++ b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs @@ -56,6 +56,7 @@ fn upgrade_ledger( max_memo_length: None, feature_flags: None, change_archive_options: None, + index_principal: None, })); env.upgrade_canister(ledger_id, ledger_wasm(), Encode!(&args).unwrap()) .unwrap() diff --git a/rs/ledger_suite/icrc1/ledger/ledger.did b/rs/ledger_suite/icrc1/ledger/ledger.did index fff3671a07b2..7eaadf46a376 100644 --- a/rs/ledger_suite/icrc1/ledger/ledger.did +++ b/rs/ledger_suite/icrc1/ledger/ledger.did @@ -117,6 +117,7 @@ type InitArgs = record { controller_id : principal; more_controller_ids : opt vec principal; }; + index_principal : opt principal; }; type ChangeFeeCollector = variant { @@ -143,6 +144,7 @@ type UpgradeArgs = record { max_memo_length : opt nat16; feature_flags : opt FeatureFlags; change_archive_options : opt ChangeArchiveOptions; + index_principal : opt principal; }; type LedgerArg = variant { @@ -506,6 +508,21 @@ type icrc103_get_allowances_response = variant { Err: GetAllowancesError; }; +type GetIndexPrincipalResult = variant { + Ok : principal; + Err : GetIndexPrincipalError; +}; + +type GetIndexPrincipalError = variant { + IndexPrincipalNotSet; + + // Any error not covered by the above variants. + GenericError: record { + error_code: nat; + description: text; + }; +}; + service : (ledger_arg : LedgerArg) -> { archives : () -> (vec ArchiveInfo) query; get_transactions : (GetTransactionsRequest) -> (GetTransactionsResponse) query; @@ -537,5 +554,7 @@ service : (ledger_arg : LedgerArg) -> { icrc103_get_allowances : (GetAllowancesArgs) -> (icrc103_get_allowances_response) query; + icrc106_get_index_principal: () -> (GetIndexPrincipalResult) query; + is_ledger_ready: () -> (bool) query; } diff --git a/rs/ledger_suite/icrc1/ledger/src/lib.rs b/rs/ledger_suite/icrc1/ledger/src/lib.rs index 6fdf979d3552..f24f1554fe99 100644 --- a/rs/ledger_suite/icrc1/ledger/src/lib.rs +++ b/rs/ledger_suite/icrc1/ledger/src/lib.rs @@ -190,6 +190,7 @@ impl InitArgsBuilder { }, max_memo_length: None, feature_flags: None, + index_principal: None, }) } @@ -249,6 +250,11 @@ impl InitArgsBuilder { self } + pub fn with_index_principal(mut self, index_principal: Principal) -> Self { + self.0.index_principal = Some(index_principal); + self + } + pub fn with_feature_flags(mut self, flags: FeatureFlags) -> Self { self.0.feature_flags = Some(flags); self @@ -272,6 +278,7 @@ pub struct InitArgs { pub archive_options: ArchiveOptions, pub max_memo_length: Option, pub feature_flags: Option, + pub index_principal: Option, } #[derive(Clone, Eq, PartialEq, Debug, CandidType, Deserialize)] @@ -348,6 +355,8 @@ pub struct UpgradeArgs { pub feature_flags: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub change_archive_options: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub index_principal: Option, } #[derive(Clone, Eq, PartialEq, Debug, Encode, Decode)] @@ -599,6 +608,9 @@ pub struct Ledger { #[serde(default)] pub ledger_version: u64, + + #[serde(default)] + index_principal: Option, } #[derive(Clone, Eq, PartialEq, Debug, CandidType, Deserialize, Serialize)] @@ -665,6 +677,7 @@ impl Ledger { fee_collector_account, max_memo_length, feature_flags, + index_principal, }: InitArgs, now: TimeStamp, ) -> Self { @@ -699,6 +712,7 @@ impl Ledger { maximum_number_of_accounts: 0, accounts_overflow_trim_quantity: 0, ledger_version: LEDGER_VERSION, + index_principal, }; if ledger.fee_collector.as_ref().map(|fc| fc.fee_collector) == Some(ledger.minting_account) @@ -882,6 +896,10 @@ impl Ledger { self.decimals } + pub fn index_principal(&self) -> Option { + self.index_principal + } + pub fn max_take_allowances(&self) -> u64 { MAX_TAKE_ALLOWANCES } @@ -910,6 +928,12 @@ impl Ledger { // (e.g. because they are fixed or computed dynamically) // please also add them to `map_metadata_or_trap` to prevent // the entry being set using init or upgrade arguments. + if let Some(index_principal) = self.index_principal() { + records.push(Value::entry( + "icrc106:index_principal", + index_principal.to_text(), + )); + } records } @@ -972,6 +996,9 @@ impl Ledger { change_archive_options.apply(archive); } } + if let Some(index_principal) = args.index_principal { + self.index_principal = Some(index_principal); + } } /// Returns the root hash of the certified ledger state. diff --git a/rs/ledger_suite/icrc1/ledger/src/main.rs b/rs/ledger_suite/icrc1/ledger/src/main.rs index dc89f3818ccc..1891082ffc08 100644 --- a/rs/ledger_suite/icrc1/ledger/src/main.rs +++ b/rs/ledger_suite/icrc1/ledger/src/main.rs @@ -34,6 +34,7 @@ use ic_stable_structures::writer::{BufferedWriter, Writer}; use icrc_ledger_types::icrc103::get_allowances::{ Allowances, GetAllowancesArgs, GetAllowancesError, }; +use icrc_ledger_types::icrc106::errors::Icrc106Error; use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError}; use icrc_ledger_types::icrc21::{ errors::Icrc21Error, lib::build_icrc21_consent_info_for_icrc1_and_icrc2_endpoints, @@ -879,6 +880,10 @@ fn supported_standards() -> Vec { name: "ICRC-103".to_string(), url: "https://github.com/dfinity/ICRC/tree/main/ICRCs/ICRC-103".to_string(), }, + StandardRecord { + name: "ICRC-106".to_string(), + url: "https://github.com/dfinity/ICRC/pull/106".to_string(), + }, ]; standards } @@ -1081,6 +1086,15 @@ fn icrc10_supported_standards() -> Vec { supported_standards() } +#[query] +#[candid_method(query)] +fn icrc106_get_index_principal() -> Result { + Access::with_ledger(|ledger| match ledger.index_principal() { + None => Err(Icrc106Error::IndexPrincipalNotSet), + Some(index_principal) => Ok(index_principal), + }) +} + #[update] #[candid_method(update)] fn icrc21_canister_call_consent_message( diff --git a/rs/ledger_suite/icrc1/ledger/src/tests.rs b/rs/ledger_suite/icrc1/ledger/src/tests.rs index c512081d4068..9b83c0c08d21 100644 --- a/rs/ledger_suite/icrc1/ledger/src/tests.rs +++ b/rs/ledger_suite/icrc1/ledger/src/tests.rs @@ -85,6 +85,7 @@ fn default_init_args() -> InitArgs { }, max_memo_length: None, feature_flags: None, + index_principal: None, } } diff --git a/rs/ledger_suite/icrc1/ledger/tests/tests.rs b/rs/ledger_suite/icrc1/ledger/tests/tests.rs index 07357e19174a..c92089472538 100644 --- a/rs/ledger_suite/icrc1/ledger/tests/tests.rs +++ b/rs/ledger_suite/icrc1/ledger/tests/tests.rs @@ -1,10 +1,10 @@ -use candid::{CandidType, Decode, Encode, Nat}; +use candid::{CandidType, Decode, Encode, Nat, Principal}; use ic_agent::identity::Identity; use ic_base_types::{CanisterId, PrincipalId}; use ic_icrc1::{Block, Operation, Transaction}; use ic_icrc1_ledger::{ ChangeFeeCollector, FeatureFlags, InitArgs, InitArgsBuilder as LedgerInitArgsBuilder, - LedgerArgument, + LedgerArgument, UpgradeArgs, }; use ic_icrc1_test_utils::minter_identity; use ic_ledger_canister_core::archive::ArchiveOptions; @@ -211,6 +211,7 @@ fn encode_init_args(args: ic_ledger_suite_state_machine_tests::InitArgs) -> Ledg archive_options: args.archive_options, max_memo_length: None, feature_flags: args.feature_flags, + index_principal: args.index_principal, }) } @@ -564,6 +565,57 @@ fn test_get_blocks_returns_multiple_archive_callbacks() { ); } +fn encode_icrc106_upgrade_args(index_principal: Option) -> LedgerArgument { + LedgerArgument::Upgrade(Some(UpgradeArgs { + metadata: None, + token_name: None, + token_symbol: None, + transfer_fee: None, + change_fee_collector: None, + max_memo_length: None, + feature_flags: None, + change_archive_options: None, + index_principal, + })) +} + +#[test] +fn test_icrc106_unsupported_if_index_not_set() { + ic_ledger_suite_state_machine_tests::icrc_106::test_icrc106_supported_even_if_index_not_set( + ledger_wasm(), + encode_init_args, + encode_icrc106_upgrade_args, + ); +} + +#[test] +fn test_icrc106_set_index_in_install() { + ic_ledger_suite_state_machine_tests::icrc_106::test_icrc106_set_index_in_install( + ledger_wasm(), + encode_init_args, + ); +} + +#[test] +fn test_icrc106_set_index_in_upgrade() { + ic_ledger_suite_state_machine_tests::icrc_106::test_icrc106_set_index_in_upgrade( + ledger_wasm(), + encode_init_args, + encode_icrc106_upgrade_args, + ); +} + +#[test] +fn test_upgrade_from_mainnet_ledger_version() { + ic_ledger_suite_state_machine_tests::icrc_106::test_upgrade_downgrade_with_mainnet_ledger( + ledger_mainnet_wasm(), + ledger_wasm(), + encode_init_args, + encode_upgrade_args, + encode_icrc106_upgrade_args, + ); +} + #[test] fn test_icrc1_test_suite() { ic_ledger_suite_state_machine_tests::test_icrc1_test_suite(ledger_wasm(), encode_init_args); @@ -945,6 +997,7 @@ fn test_icrc2_feature_flag_doesnt_disable_icrc2_endpoints() { }, max_memo_length: None, feature_flags: Some(FeatureFlags { icrc2: false }), + index_principal: None, })) .unwrap(); let ledger_id = env @@ -1119,6 +1172,7 @@ fn test_icrc3_get_archives() { }, max_memo_length: None, feature_flags: None, + index_principal: None, }); let args = Encode!(&args).unwrap(); let ledger_id = env @@ -1194,6 +1248,7 @@ fn test_icrc3_get_blocks() { }, max_memo_length: None, feature_flags: None, + index_principal: None, }); let args = Encode!(&args).unwrap(); let ledger_id = env @@ -1468,6 +1523,7 @@ fn test_icrc3_get_blocks_number_of_blocks_limit() { }, max_memo_length: None, feature_flags: None, + index_principal: None, }); let args = Encode!(&args).unwrap(); @@ -2028,6 +2084,7 @@ mod verify_written_blocks { }, max_memo_length: None, feature_flags: Some(FeatureFlags { icrc2: true }), + index_principal: None, }); let args = Encode!(&ledger_arg_init).unwrap(); @@ -2244,6 +2301,7 @@ mod incompatible_token_type_upgrade { }, max_memo_length: None, feature_flags: Some(FeatureFlags { icrc2: false }), + index_principal: None, })) .unwrap() } diff --git a/rs/ledger_suite/tests/sm-tests/src/icrc_106.rs b/rs/ledger_suite/tests/sm-tests/src/icrc_106.rs new file mode 100644 index 000000000000..9f4bbd267d42 --- /dev/null +++ b/rs/ledger_suite/tests/sm-tests/src/icrc_106.rs @@ -0,0 +1,205 @@ +use super::*; + +pub fn test_icrc106_supported_even_if_index_not_set( + ledger_wasm: Vec, + encode_ledger_init_args: fn(InitArgs) -> T, + encode_upgrade_args: fn(Option) -> U, +) where + T: CandidType, + U: CandidType, +{ + let env = StateMachine::new(); + let ledger_canister_id = env.create_canister(None); + let ledger_init_args = encode_ledger_init_args(init_args(vec![])); + env.install_existing_canister( + ledger_canister_id, + ledger_wasm.clone(), + Encode!(&ledger_init_args).unwrap(), + ) + .expect("should successfully install ledger canister"); + + assert_index_not_set(&env, ledger_canister_id, true); + + let args = encode_upgrade_args(None); + let encoded_upgrade_args = Encode!(&args).unwrap(); + env.upgrade_canister( + ledger_canister_id, + ledger_wasm, + encoded_upgrade_args.clone(), + ) + .expect("should successfully upgrade ledger canister"); + + assert_index_not_set(&env, ledger_canister_id, true); +} + +pub fn test_icrc106_set_index_in_install( + ledger_wasm: Vec, + encode_ledger_init_args: fn(InitArgs) -> T, +) where + T: CandidType, +{ + let env = StateMachine::new(); + let ledger_canister_id = env.create_canister(None); + let index_canister_id = env.create_canister(None); + let index_principal = Principal::from(index_canister_id.get()); + let ledger_init_args = encode_ledger_init_args(InitArgs { + index_principal: Some(index_principal), + ..init_args(vec![]) + }); + env.install_existing_canister( + ledger_canister_id, + ledger_wasm.clone(), + Encode!(&ledger_init_args).unwrap(), + ) + .expect("should successfully install ledger canister"); + + assert_index_set(&env, ledger_canister_id, index_principal); +} + +pub fn test_icrc106_set_index_in_upgrade( + ledger_wasm: Vec, + encode_init_args: fn(InitArgs) -> T, + encode_upgrade_args: fn(Option) -> U, +) where + T: CandidType, + U: CandidType, +{ + let (env, canister_id) = setup(ledger_wasm.clone(), encode_init_args, vec![]); + assert_index_not_set(&env, canister_id, true); + + let index_principal = PrincipalId::new_user_test_id(1).0; + let args = encode_upgrade_args(Some(index_principal)); + let encoded_upgrade_args = Encode!(&args).unwrap(); + env.upgrade_canister(canister_id, ledger_wasm.clone(), encoded_upgrade_args) + .expect("should successfully upgrade ledger canister"); + // The index should now be set + assert_index_set(&env, canister_id, index_principal); + + let args = encode_upgrade_args(None); + let encoded_upgrade_args = Encode!(&args).unwrap(); + env.upgrade_canister(canister_id, ledger_wasm, encoded_upgrade_args) + .expect("should successfully upgrade ledger canister"); + // Passing `None` should not change the previously set index + assert_index_set(&env, canister_id, index_principal); +} + +pub fn test_upgrade_downgrade_with_mainnet_ledger( + mainnet_ledger_wasm: Vec, + ledger_wasm: Vec, + encode_init_args: fn(InitArgs) -> T, + encode_empty_upgrade_args: fn() -> U, + encode_upgrade_args: fn(Option) -> U, +) where + T: CandidType, + U: CandidType, +{ + // Install the mainnet ledger canister that does not support ICRC-106 + let (env, canister_id) = setup(mainnet_ledger_wasm.clone(), encode_init_args, vec![]); + assert_index_not_set(&env, canister_id, false); + + // Upgrade to a ledger version that supports ICRC-106, but does not set the index principal + let encoded_empty_upgrade_args = Encode!(&encode_empty_upgrade_args()).unwrap(); + env.upgrade_canister( + canister_id, + ledger_wasm.clone(), + encoded_empty_upgrade_args.clone(), + ) + .expect("should successfully upgrade ledger canister"); + assert_index_not_set(&env, canister_id, true); + + // Self-upgrade to a ledger version and set the index principal + let index_principal = PrincipalId::new_user_test_id(1).0; + let args = encode_upgrade_args(Some(index_principal)); + let encoded_upgrade_args = Encode!(&args).unwrap(); + env.upgrade_canister(canister_id, ledger_wasm.clone(), encoded_upgrade_args) + .expect("should successfully upgrade ledger canister"); + assert_index_set(&env, canister_id, index_principal); + + // Self-upgrade the ledger with empty upgrade args. The index principal should stay set. + let encoded_empty_upgrade_args = Encode!(&encode_empty_upgrade_args()).unwrap(); + env.upgrade_canister( + canister_id, + ledger_wasm.clone(), + encoded_empty_upgrade_args.clone(), + ) + .expect("should successfully self-upgrade ledger canister"); + assert_index_set(&env, canister_id, index_principal); + + // Downgrade the ledger to the mainnet version that does not support ICRC-106 + env.upgrade_canister(canister_id, mainnet_ledger_wasm, encoded_empty_upgrade_args) + .expect("should successfully downgrade ledger canister"); + assert_index_not_set(&env, canister_id, false); + + // Upgrade to a ledger version that supports ICRC-106, but do not set the index principal + let encoded_empty_upgrade_args = Encode!(&encode_empty_upgrade_args()).unwrap(); + env.upgrade_canister(canister_id, ledger_wasm, encoded_empty_upgrade_args) + .expect("should successfully upgrade ledger canister"); + assert_index_not_set(&env, canister_id, true); +} + +fn assert_index_not_set( + env: &StateMachine, + ledger_canister_id: CanisterId, + expect_icrc106_supported: bool, +) { + check_icrc106_support(env, ledger_canister_id, expect_icrc106_supported); + if expect_icrc106_supported { + assert_eq!( + Err(Icrc106Error::IndexPrincipalNotSet), + icrc106_get_index_principal(env, ledger_canister_id) + ); + } + assert_eq!( + None, + metadata(env, ledger_canister_id).get("icrc106:index_principal") + ); +} + +fn assert_index_set( + env: &StateMachine, + ledger_canister_id: CanisterId, + index_principal: Principal, +) { + check_icrc106_support(env, ledger_canister_id, true); + assert_eq!( + Ok(index_principal), + icrc106_get_index_principal(env, ledger_canister_id) + ); + assert_eq!( + &Value::Text(index_principal.to_text()), + metadata(env, ledger_canister_id) + .get("icrc106:index_principal") + .expect("should have index principal metadata") + ); +} + +fn check_icrc106_support( + env: &StateMachine, + canister_id: CanisterId, + expect_icrc106_supported: bool, +) { + let mut found = false; + for standard in supported_standards(env, canister_id) { + if standard.name == "ICRC-106" { + found = true; + break; + } + } + assert_eq!( + found, expect_icrc106_supported, + "ICRC-106 should be supported" + ); +} + +fn icrc106_get_index_principal( + env: &StateMachine, + ledger: CanisterId, +) -> Result { + Decode!( + &env.query(ledger, "icrc106_get_index_principal", Encode!().unwrap()) + .expect("failed to query icrc106_get_index_principal") + .bytes(), + Result + ) + .expect("failed to decode icrc106_get_index_principal response") +} diff --git a/rs/ledger_suite/tests/sm-tests/src/lib.rs b/rs/ledger_suite/tests/sm-tests/src/lib.rs index c43bf7c0b6c9..a0dd4cc94b57 100644 --- a/rs/ledger_suite/tests/sm-tests/src/lib.rs +++ b/rs/ledger_suite/tests/sm-tests/src/lib.rs @@ -33,6 +33,7 @@ use icrc_ledger_types::icrc::generic_value::Value as GenericValue; use icrc_ledger_types::icrc1::account::{Account, Subaccount, DEFAULT_SUBACCOUNT}; use icrc_ledger_types::icrc1::transfer::{Memo, TransferArg, TransferError}; use icrc_ledger_types::icrc103::get_allowances::{Allowances, GetAllowancesArgs}; +use icrc_ledger_types::icrc106::errors::Icrc106Error; use icrc_ledger_types::icrc2::allowance::{Allowance, AllowanceArgs}; use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError}; use icrc_ledger_types::icrc2::transfer_from::{TransferFromArgs, TransferFromError}; @@ -66,6 +67,7 @@ use std::{ mod allowances; pub mod fee_collector; +pub mod icrc_106; pub mod in_memory_ledger; pub mod metrics; @@ -105,6 +107,7 @@ pub struct InitArgs { pub metadata: Vec<(String, Value)>, pub archive_options: ArchiveOptions, pub feature_flags: Option, + pub index_principal: Option, } #[derive(Clone, Eq, PartialEq, Debug, CandidType)] @@ -895,6 +898,7 @@ fn init_args(initial_balances: Vec<(Account, u64)>) -> InitArgs { max_transactions_per_response: None, }, feature_flags: Some(FeatureFlags { icrc2: true }), + index_principal: None, } } @@ -1090,7 +1094,7 @@ where standards.sort(); assert_eq!( standards, - vec!["ICRC-1", "ICRC-10", "ICRC-103", "ICRC-2", "ICRC-21", "ICRC-3"] + vec!["ICRC-1", "ICRC-10", "ICRC-103", "ICRC-106", "ICRC-2", "ICRC-21", "ICRC-3"] ); } @@ -1668,6 +1672,7 @@ pub fn test_archive_controllers(ledger_wasm: Vec) { max_transactions_per_response: None, }, feature_flags: args.feature_flags, + index_principal: None, }) } @@ -1696,6 +1701,7 @@ pub fn test_archive_no_additional_controllers(ledger_wasm: Vec) { max_transactions_per_response: None, }, feature_flags: args.feature_flags, + index_principal: None, }) } @@ -1729,6 +1735,7 @@ pub fn test_archive_duplicate_controllers(ledger_wasm: Vec) { max_transactions_per_response: None, }, feature_flags: args.feature_flags, + index_principal: None, }) } let p100 = PrincipalId::new_user_test_id(100); diff --git a/rs/sns/governance/src/types/tests.rs b/rs/sns/governance/src/types/tests.rs index 8ada1901639a..ac8d77aedd21 100644 --- a/rs/sns/governance/src/types/tests.rs +++ b/rs/sns/governance/src/types/tests.rs @@ -1578,6 +1578,7 @@ fn test_from_manage_ledger_parameters_into_ledger_upgrade_args() { max_memo_length: None, feature_flags: None, change_archive_options: None, + index_principal: None, } ); } @@ -1604,6 +1605,7 @@ fn test_from_manage_ledger_parameters_into_ledger_upgrade_args_no_logo() { max_memo_length: None, feature_flags: None, change_archive_options: None, + index_principal: None, } ); } diff --git a/rs/sns/governance/unreleased_changelog.md b/rs/sns/governance/unreleased_changelog.md index 94126a0ff421..805c60d55680 100644 --- a/rs/sns/governance/unreleased_changelog.md +++ b/rs/sns/governance/unreleased_changelog.md @@ -4,11 +4,16 @@ In general, upcoming/unreleased behavior changes are described here. For details on the process that this file is part of, see `rs/nervous_system/changelog_process.md`. - # Next Upgrade Proposal ## Added +### Set the principal of the index canister when installing the ledger ([ICRC-106](https://github.com/dfinity/ICRC-1/pull/196/files/7f9b4739d9b3ec2cf549bf468e3a1731c31eecbf)) + +When installing the ledger canister for a new SNS, the index canister's principal is now set in the ledger. +This allows a ledger client to query the ledger using the `icrc106_get_index_principal` endpoint to figure out where the +ledger index canister is running. + ## Changed ## Deprecated diff --git a/rs/sns/init/src/lib.rs b/rs/sns/init/src/lib.rs index 412ae5db4af9..09e9a9178e31 100644 --- a/rs/sns/init/src/lib.rs +++ b/rs/sns/init/src/lib.rs @@ -613,7 +613,8 @@ impl SnsInitPayload { // 10 Trillion cycles cycles_for_archive_creation: Some(10_000_000_000_000), max_transactions_per_response: None, - }); + }) + .with_index_principal(Principal::from(sns_canister_ids.index)); if let Some(token_logo) = &self.token_logo { payload_builder = payload_builder.with_metadata_entry(