diff --git a/Cargo.lock b/Cargo.lock index 8612da460e5..f49e82dfdf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8392,6 +8392,7 @@ dependencies = [ "hex", "ic-base-types", "ic-crypto-sha2", + "ic-icrc1-index-ng", "ic-icrc1-ledger", "ic-icrc1-ledger-sm-tests", "ic-icrc1-test-utils", @@ -8402,6 +8403,7 @@ dependencies = [ "ic-ledger-hash-of", "ic-nns-test-utils", "ic-nns-test-utils-golden-nns-state", + "ic-registry-subnet-type", "ic-state-machine-tests", "ic-test-utilities-load-wasm", "icrc-ledger-types", @@ -13089,6 +13091,7 @@ dependencies = [ name = "icp-rosetta-integration-tests" version = "0.9.0" dependencies = [ + "anyhow", "candid", "ic-agent", "ic-icrc-rosetta", diff --git a/ic-os/defs.bzl b/ic-os/defs.bzl index 7ff5f175ccc..4e5dc38045e 100644 --- a/ic-os/defs.bzl +++ b/ic-os/defs.bzl @@ -322,20 +322,6 @@ def icos_build( tags = ["manual"], ) - gzip_compress( - name = "disk-img.tar.gz", - srcs = [":disk-img.tar"], - visibility = visibility, - tags = ["manual"], - ) - - sha256sum( - name = "disk-img.tar.gz.sha256", - srcs = [":disk-img.tar.gz"], - visibility = visibility, - tags = ["manual"], - ) - # -------------------- Assemble upgrade image -------------------- if upgrades: @@ -444,7 +430,6 @@ def icos_build( name = "upload_disk-img", inputs = [ ":disk-img.tar.zst", - ":disk-img.tar.gz", ], remote_subdir = upload_prefix + "/disk-img" + upload_suffix, visibility = visibility, @@ -458,14 +443,6 @@ def icos_build( tags = ["manual"], ) - output_files( - name = "disk-img-url-gz", - target = ":upload_disk-img", - basenames = ["upload_disk-img_disk-img.tar.gz.url"], - visibility = visibility, - tags = ["manual"], - ) - if upgrades: upload_artifacts( name = "upload_update-img", @@ -629,7 +606,6 @@ EOF testonly = malicious, srcs = [ ":disk-img.tar.zst", - ":disk-img.tar.gz", ] + ([ ":update-img.tar.zst", ":update-img.tar.gz", diff --git a/mainnet-canisters.bzl b/mainnet-canisters.bzl index 7a501ea13e1..91a0d9c0284 100644 --- a/mainnet-canisters.bzl +++ b/mainnet-canisters.bzl @@ -1,5 +1,5 @@ """ -This module defines Bazel targets for the mainnet versions of the core NNS and SNS canisters. +This module defines Bazel targets for the mainnet versions of the core NNS, SNS, and ck canisters. """ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") @@ -22,7 +22,9 @@ CANISTER_NAME_TO_WASM_METADATA = { "sns_index": ("35e4f2c583b0657aa730740b5c8aca18a8718b8e", "110352d412a97dce090dd902e9dbdc874211d0e7a5179b6814ec1694e45a2807"), "sns_ledger": ("35e4f2c583b0657aa730740b5c8aca18a8718b8e", "26de3e745b0e98cc83850ebf0f8fd1a574905bf7c73d52fcf61ee3f35e4875e1"), "sns_archive": ("35e4f2c583b0657aa730740b5c8aca18a8718b8e", "ea2df4e0e3f4e5e91d43baf281728b2443ab3236ba473d78913cfbe2b5763d3c"), + "ck_btc_index": ("a3831c87440df4821b435050c8a8fcb3745d86f6", "cac207cf438df8c9fba46d4445c097f05fd8228a1eeacfe0536b7e9ddefc5f1c"), "ck_btc_ledger": ("a3831c87440df4821b435050c8a8fcb3745d86f6", "4264ce2952c4e9ff802d81a11519d5e3ffdaed4215d5831a6634e59efd72f7d8"), + "ck_eth_index": ("a3831c87440df4821b435050c8a8fcb3745d86f6", "8104acad6105abb069b2dbc8289692bd63c2d110127f8e91f99db51465962606"), "ck_eth_ledger": ("a3831c87440df4821b435050c8a8fcb3745d86f6", "e5c8a297d1c0c6d2ab2253c0280aaefd6e23fe3a8a994fc64706a1f3c3116062"), } @@ -132,6 +134,14 @@ def mainnet_ck_canisters(): url = canister_url(git_commit_id, "ic-icrc1-ledger.wasm.gz"), ) + git_commit_id, sha256 = CANISTER_NAME_TO_WASM_METADATA["ck_btc_index"] + http_file( + name = "mainnet_ckbtc-index-ng", + downloaded_file_path = "ic-icrc1-index-ng.wasm.gz", + sha256 = sha256, + url = canister_url(git_commit_id, "ic-icrc1-index-ng.wasm.gz"), + ) + git_commit_id, sha256 = CANISTER_NAME_TO_WASM_METADATA["ck_eth_ledger"] http_file( name = "mainnet_cketh_ic-icrc1-ledger-u256", @@ -140,6 +150,14 @@ def mainnet_ck_canisters(): url = canister_url(git_commit_id, "ic-icrc1-ledger-u256.wasm.gz"), ) + git_commit_id, sha256 = CANISTER_NAME_TO_WASM_METADATA["ck_eth_index"] + http_file( + name = "mainnet_cketh-index-ng", + downloaded_file_path = "ic-icrc1-index-ng-u256.wasm.gz", + sha256 = sha256, + url = canister_url(git_commit_id, "ic-icrc1-index-ng-u256.wasm.gz"), + ) + def mainnet_sns_canisters(): """ Provides Bazel targets for the latest SNS canisters published to the mainnet SNS-W. diff --git a/rs/nns/governance/src/governance.rs b/rs/nns/governance/src/governance.rs index 95dc6797dc3..0d89b8f3114 100644 --- a/rs/nns/governance/src/governance.rs +++ b/rs/nns/governance/src/governance.rs @@ -962,6 +962,11 @@ impl Action { } Action::ExecuteNnsFunction(execute_nns_function) } + Action::InstallCode(mut install_code) => { + install_code.wasm_module = None; + install_code.arg = None; + Action::InstallCode(install_code) + } action => action, } } @@ -3646,13 +3651,16 @@ impl Governance { let voting_period_seconds = self.voting_period_seconds()(topic); let reward_status = data.reward_status(now_seconds, voting_period_seconds); - // If this is part of a "multi" query and an ExecuteNnsFunction - // proposal then remove the payload if the payload is larger - // than EXECUTE_NNS_FUNCTION_PAYLOAD_LISTING_BYTES_MAX. + // For multi-queries, large fields such as WASM blobs need to be omitted. Otherwise the + // message limit will be exceeded. let proposal = if multi_query { if let Some( proposal @ Proposal { - action: Some(proposal::Action::ExecuteNnsFunction(_)), + action: + Some( + proposal::Action::ExecuteNnsFunction(_) + | proposal::Action::InstallCode(_), + ), .. }, ) = data.proposal.clone() diff --git a/rs/nns/governance/tests/governance.rs b/rs/nns/governance/tests/governance.rs index ab24b5bb51f..8601ee9eed0 100644 --- a/rs/nns/governance/tests/governance.rs +++ b/rs/nns/governance/tests/governance.rs @@ -60,6 +60,7 @@ use ic_nns_governance::{ governance_error::ErrorType::{ self, InsufficientFunds, NotAuthorized, NotFound, PreconditionFailed, ResourceExhausted, }, + install_code::CanisterInstallMode, manage_neuron::{ self, claim_or_refresh::{By, MemoAndController}, @@ -78,7 +79,7 @@ use ic_nns_governance::{ AddOrRemoveNodeProvider, ApproveGenesisKyc, Ballot, BallotChange, BallotInfo, BallotInfoChange, CreateServiceNervousSystem, Empty, ExecuteNnsFunction, Governance as GovernanceProto, GovernanceChange, GovernanceError, - IdealMatchedParticipationFunction, KnownNeuron, KnownNeuronData, ListNeurons, + IdealMatchedParticipationFunction, InstallCode, KnownNeuron, KnownNeuronData, ListNeurons, ListProposalInfo, ListProposalInfoResponse, ManageNeuron, ManageNeuronResponse, MonthlyNodeProviderRewards, Motion, NetworkEconomics, Neuron, NeuronChange, NeuronState, NeuronType, NeuronsFundData, NeuronsFundParticipation, NeuronsFundSnapshot, NnsFunction, @@ -7589,6 +7590,68 @@ fn test_list_proposals_removes_execute_nns_function_payload() { ); } +#[test] +fn test_list_proposals_removes_install_code_large_fields() { + let original_install_code = InstallCode { + wasm_module: Some(vec![0; 100_000]), + arg: Some(vec![0; 1_000]), + install_mode: Some(CanisterInstallMode::Upgrade as i32), + canister_id: Some(GOVERNANCE_CANISTER_ID.into()), + skip_stopping_before_installing: Some(false), + }; + let proto = GovernanceProto { + economics: Some(NetworkEconomics::with_default_values()), + proposals: btreemap! { + 1 => ProposalData { + id: Some(ProposalId { id: 1 }), + proposal: Some(Proposal { + title: Some("Upgrade canister".to_string()), + action: Some(proposal::Action::InstallCode(original_install_code.clone())), + ..Default::default() + }), + ..Default::default() + } + }, + ..Default::default() + }; + let driver = fake::FakeDriver::default(); + let gov = Governance::new( + proto, + driver.get_fake_env(), + driver.get_fake_ledger(), + driver.get_fake_cmc(), + ); + let caller = &principal(1); + + let results = gov.list_proposals( + caller, + &ListProposalInfo { + ..Default::default() + }, + ); + + let action = results.proposal_info[0] + .proposal + .as_ref() + .unwrap() + .action + .as_ref() + .unwrap(); + match action { + proposal::Action::InstallCode(install_code) => { + assert_eq!( + install_code, + &InstallCode { + wasm_module: None, + arg: None, + ..original_install_code + } + ); + } + _ => panic!("Unexpected action"), + }; +} + #[test] fn test_list_proposals_retains_execute_nns_function_payload() { // ARRANGE diff --git a/rs/rosetta-api/icp_ledger/rosetta-integration-tests/BUILD.bazel b/rs/rosetta-api/icp_ledger/rosetta-integration-tests/BUILD.bazel index 0f93b1563fa..856b4cbb4a8 100644 --- a/rs/rosetta-api/icp_ledger/rosetta-integration-tests/BUILD.bazel +++ b/rs/rosetta-api/icp_ledger/rosetta-integration-tests/BUILD.bazel @@ -16,6 +16,7 @@ DEPENDENCIES = [ "//rs/rosetta-api/icrc1/rosetta/client:ic-icrc-rosetta-client", "//rs/rosetta-api/rosetta_core:rosetta-core", "//rs/test_utilities/load_wasm", + "@crate_index//:anyhow", "@crate_index//:candid", "@crate_index//:ic-agent", "@crate_index//:num-traits", diff --git a/rs/rosetta-api/icp_ledger/rosetta-integration-tests/Cargo.toml b/rs/rosetta-api/icp_ledger/rosetta-integration-tests/Cargo.toml index 2352610126f..4f46aab3a06 100644 --- a/rs/rosetta-api/icp_ledger/rosetta-integration-tests/Cargo.toml +++ b/rs/rosetta-api/icp_ledger/rosetta-integration-tests/Cargo.toml @@ -28,4 +28,5 @@ serde_json = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } url = { workspace = true } -num-traits = { workspace = true } \ No newline at end of file +num-traits = { workspace = true } +anyhow = { workspace = true } \ No newline at end of file diff --git a/rs/rosetta-api/icp_ledger/rosetta-integration-tests/src/lib.rs b/rs/rosetta-api/icp_ledger/rosetta-integration-tests/src/lib.rs index ef4ab3665a2..04e8d0b87af 100644 --- a/rs/rosetta-api/icp_ledger/rosetta-integration-tests/src/lib.rs +++ b/rs/rosetta-api/icp_ledger/rosetta-integration-tests/src/lib.rs @@ -45,6 +45,7 @@ pub async fn start_rosetta( ledger_canister_id: Principal, state_directory: Option, enable_rosetta_blocks: bool, + persistent_storage: bool, ) -> (RosettaClient, RosettaContext) { assert!( rosetta_bin.exists(), @@ -68,16 +69,20 @@ pub async fn start_rosetta( } } } - let store_location = state_directory.join("data"); let mut cmd = Command::new(rosetta_bin); cmd.arg("--ic-url") .arg(ic_url.to_string()) .arg("--canister-id") .arg(ledger_canister_id.to_string()) .arg("--port-file") - .arg(port_file.clone()) - .arg("--store-location") - .arg(store_location.clone()); + .arg(port_file.clone()); + + if persistent_storage { + let store_location = state_directory.join("data"); + cmd.arg("--store-location").arg(store_location); + } else { + cmd.arg("--store-type").arg("sqlite-in-memory"); + } if enable_rosetta_blocks { cmd.arg("--enable-rosetta-blocks"); diff --git a/rs/rosetta-api/icp_ledger/rosetta-integration-tests/tests/tests.rs b/rs/rosetta-api/icp_ledger/rosetta-integration-tests/tests/tests.rs index 80ce594f615..ab05ba5d1f9 100644 --- a/rs/rosetta-api/icp_ledger/rosetta-integration-tests/tests/tests.rs +++ b/rs/rosetta-api/icp_ledger/rosetta-integration-tests/tests/tests.rs @@ -29,8 +29,8 @@ use tempfile::TempDir; use url::Url; pub const LEDGER_CANISTER_INDEX_IN_NNS_SUBNET: u64 = 2; -const MAX_ATTEMPTS: u16 = 1000; -const DURATION_BETWEEN_ATTEMPTS: Duration = Duration::from_millis(100); +const MAX_ATTEMPTS: u16 = 10; +const DURATION_BETWEEN_ATTEMPTS: Duration = Duration::from_millis(1000); fn get_rosetta_path() -> std::path::PathBuf { std::fs::canonicalize(std::env::var_os("ROSETTA_PATH").expect("missing ic-rosetta-api binary")) @@ -102,14 +102,12 @@ struct RosettaTestingClient { } impl RosettaTestingClient { - async fn block_or_panic(&self, id: PartialBlockIdentifier) -> rosetta_core::objects::Block { + async fn block( + &self, + id: PartialBlockIdentifier, + ) -> Result { let network = self.network_or_panic().await; - self.rosetta_client - .block(network, id.clone()) - .await - .expect("Unable to call /block") - .block - .unwrap_or_else(|| panic!("Block with id {id:?} not found")) + self.rosetta_client.block(network, id.clone()).await } async fn block_transaction( @@ -138,12 +136,9 @@ impl RosettaTestingClient { networks.remove(0) } - async fn network_status_or_panic(&self) -> NetworkStatusResponse { + async fn network_status(&self) -> Result { let network = self.network_or_panic().await; - self.rosetta_client - .network_status(network) - .await - .expect("Unable to call /network/status") + self.rosetta_client.network_status(network).await } async fn status_or_panic(&self) -> RosettaStatus { @@ -159,20 +154,23 @@ impl RosettaTestingClient { .expect("Unable to parse response body for /status") } - async fn wait_or_panic_until_synced_up_to(&self, block_index: u64) { - let mut network_status = self.network_status_or_panic().await; + async fn wait_until_synced_up_to(&self, block_index: u64) -> anyhow::Result<()> { let mut attempts = 0; - while network_status.current_block_identifier.index < block_index { + loop { + let status = self.network_status().await; + if status.is_ok() && status.unwrap().current_block_identifier.index >= block_index { + break; + } if attempts >= MAX_ATTEMPTS { - panic!( - "Rosetta was unable to sync up to block index: {}. Last network status was: {:#?}", - block_index, network_status + anyhow::bail!( + "Rosetta was unable to sync up to block index: {}", + block_index ); } attempts += 1; sleep(DURATION_BETWEEN_ATTEMPTS); - network_status = self.network_status_or_panic().await; } + Ok(()) } pub async fn call_or_panic(&self, method: String, arg: ObjectMap) -> CallResponse { @@ -221,6 +219,7 @@ impl TestEnv { ledger_canister_id: Principal, rosetta_state_directory: PathBuf, enable_rosetta_blocks: bool, + persistent_storage: bool, ) -> (RosettaClient, RosettaContext) { let (rosetta_client, rosetta_context) = start_rosetta( &get_rosetta_path(), @@ -228,6 +227,7 @@ impl TestEnv { ledger_canister_id, Some(rosetta_state_directory), enable_rosetta_blocks, + persistent_storage, ) .await; @@ -257,7 +257,6 @@ impl TestEnv { break; } Err(Error(err)) if matches_blockchain_is_empty_error(&err) => { - println!("Found \"Blockchain is empty\" error, retrying in {DURATION_BETWEEN_ATTEMPTS:?} (retries: {retries})"); retries -= 1; sleep(DURATION_BETWEEN_ATTEMPTS); } @@ -270,73 +269,88 @@ impl TestEnv { (rosetta_client, rosetta_context) } - async fn setup_or_panic(enable_rosetta_blocks: bool) -> Self { - let mut pocket_ic = PocketIcBuilder::new().with_nns_subnet().build_async().await; - - let sender_canister_id = pocket_ic.create_canister().await; - pocket_ic - .install_canister( - sender_canister_id, - sender_wasm_bytes(), - Encode!().unwrap(), - None, - ) - .await; - pocket_ic - .add_cycles(sender_canister_id, STARTING_CYCLES_PER_CANISTER) - .await; - - let ledger_canister_id = Principal::from(LEDGER_CANISTER_ID); - let canister_id = pocket_ic - .create_canister_with_id(None, None, ledger_canister_id) - .await - .expect("Unable to create the canister in which the Ledger would be installed"); - pocket_ic - .install_canister( - canister_id, - icp_ledger_wasm_bytes(), - icp_ledger_init(sender_canister_id), - None, + async fn setup(enable_rosetta_blocks: bool, persistent_storage: bool) -> anyhow::Result { + let mut attempts = 2; + loop { + let mut pocket_ic = PocketIcBuilder::new().with_nns_subnet().build_async().await; + + let sender_canister_id = pocket_ic.create_canister().await; + pocket_ic + .install_canister( + sender_canister_id, + sender_wasm_bytes(), + Encode!().unwrap(), + None, + ) + .await; + pocket_ic + .add_cycles(sender_canister_id, STARTING_CYCLES_PER_CANISTER) + .await; + + let ledger_canister_id = Principal::from(LEDGER_CANISTER_ID); + let canister_id = pocket_ic + .create_canister_with_id(None, None, ledger_canister_id) + .await + .expect("Unable to create the canister in which the Ledger would be installed"); + pocket_ic + .install_canister( + canister_id, + icp_ledger_wasm_bytes(), + icp_ledger_init(sender_canister_id), + None, + ) + .await; + const STARTING_CYCLES_PER_CANISTER: u128 = 2_000_000_000_000_000; + pocket_ic + .add_cycles(canister_id, STARTING_CYCLES_PER_CANISTER) + .await; + println!( + "Installed the Ledger canister ({canister_id}) onto {}", + pocket_ic.get_subnet(canister_id).await.unwrap() + ); + + let replica_url = pocket_ic.make_live(None).await; + + let rosetta_state_directory = + TempDir::new().expect("failed to create a temporary directory"); + + let (rosetta_client, rosetta_context) = Self::setup_rosetta( + replica_url, + ledger_canister_id, + rosetta_state_directory.path().to_owned(), + enable_rosetta_blocks, + persistent_storage, ) .await; - const STARTING_CYCLES_PER_CANISTER: u128 = 2_000_000_000_000_000; - pocket_ic - .add_cycles(canister_id, STARTING_CYCLES_PER_CANISTER) - .await; - println!( - "Installed the Ledger canister ({canister_id}) onto {}", - pocket_ic.get_subnet(canister_id).await.unwrap() - ); - let replica_url = pocket_ic.make_live(None).await; - - let rosetta_state_directory = - TempDir::new().expect("failed to create a temporary directory"); - - let (rosetta_client, rosetta_context) = Self::setup_rosetta( - replica_url, - ledger_canister_id, - rosetta_state_directory.path().to_owned(), - enable_rosetta_blocks, - ) - .await; - - let env = TestEnv::new( - rosetta_state_directory, - pocket_ic, - rosetta_context, - rosetta_client, - ledger_canister_id, - sender_canister_id, - ); - - // block 0 always exists in this setup - env.rosetta.wait_or_panic_until_synced_up_to(0).await; - - env + let env = TestEnv::new( + rosetta_state_directory, + pocket_ic, + rosetta_context, + rosetta_client, + ledger_canister_id, + sender_canister_id, + ); + + // block 0 always exists in this setup + match env.rosetta.wait_until_synced_up_to(0).await { + Ok(_) => break Ok(env), + Err(e) => { + println!("Error during setup waiting for Rosetta to sync up to block 0: {e}"); + if attempts == 0 { + anyhow::bail!("Unable to setup TestEnv"); + } + attempts -= 1; + } + } + } } - async fn restart_rosetta_node(&mut self, enable_rosetta_blocks: bool) { + async fn restart_rosetta_node( + &mut self, + enable_rosetta_blocks: bool, + persistent_storage: bool, + ) -> anyhow::Result<()> { let rosetta_state_directory; if let Some(rosetta_context) = std::mem::take(&mut self.rosetta_context) { rosetta_state_directory = rosetta_context.state_directory.clone(); @@ -356,13 +370,12 @@ impl TestEnv { ledger_canister_id, rosetta_state_directory, enable_rosetta_blocks, + persistent_storage, ) .await; self.rosetta = RosettaTestingClient { rosetta_client }; self.rosetta_context = Some(rosetta_context); - - // block 0 always exists in this setup - self.rosetta.wait_or_panic_until_synced_up_to(0).await; + Ok(()) } pub async fn icrc1_transfers(&self, args: Vec) -> Vec { @@ -392,19 +405,22 @@ impl TestEnv { } fn matches_blockchain_is_empty_error(error: &rosetta_core::miscellaneous::Error) -> bool { - error.code == 700 + (error.code == 700 || error.code == 712 || error.code == 721) && error.details.is_some() && error .details .as_ref() .unwrap() .get("error_message") - .map_or(false, |e| e == "Blockchain is empty") + .map_or(false, |e| { + e == "Blockchain is empty" || e == "Block not found: 0" || e == "RosettaBlocks was activated and there are no RosettaBlocks in the database yet. The synch is ongoing, please wait until the first RosettaBlock is written to the database." + }) } #[tokio::test] async fn test_rosetta_blocks_mode_enabled() { - let mut env = TestEnv::setup_or_panic(false).await; + let mut env = TestEnv::setup(false, true).await.unwrap(); + // Check that by default the rosetta blocks mode is not enabled assert_eq!( env.rosetta.status_or_panic().await.rosetta_blocks_mode, @@ -413,14 +429,14 @@ async fn test_rosetta_blocks_mode_enabled() { // Check that restarting Rosetta doesn't enable the // rosetta blocks mode - env.restart_rosetta_node(false).await; + env.restart_rosetta_node(false, true).await.unwrap(); assert_eq!( env.rosetta.status_or_panic().await.rosetta_blocks_mode, RosettaBlocksMode::Disabled ); // Check that restarting Rosetta doesn't enable the // rosetta blocks mode - env.restart_rosetta_node(false).await; + env.restart_rosetta_node(false, true).await.unwrap(); assert_eq!( env.rosetta.status_or_panic().await.rosetta_blocks_mode, RosettaBlocksMode::Disabled @@ -433,28 +449,44 @@ async fn test_rosetta_blocks_mode_enabled() { // of the next block to sync (i.e. current block index + 1) let first_rosetta_block_index = env .rosetta - .network_status_or_panic() + .network_status() .await + .unwrap() .current_block_identifier .index + 1; - env.restart_rosetta_node(true).await; + + env.restart_rosetta_node(true, true).await.unwrap(); assert_eq!( env.rosetta.status_or_panic().await.rosetta_blocks_mode, RosettaBlocksMode::Enabled { first_rosetta_block_index } ); - // The first rosetta block index is the same as the index - // of the next block to sync (i.e. current block index + 1) + + // Currently there exists no rosetta block. + // We need to create one or otherwise rosetta will simply return an error stating that the blockchain is empty + assert!(env.rosetta.network_status().await.is_err()); + env.icrc1_transfers(vec![TransferArg { + from_subaccount: None, + to: Account::from(Principal::anonymous()), + fee: None, + created_at_time: None, + memo: None, + amount: Nat::from(1u64), + }]) + .await; + // Let rosetta catch up to the latest block + env.rosetta.wait_until_synced_up_to(1).await.unwrap(); + // The first rosetta block index is the same as the index of the most recently fetched block let first_rosetta_block_index = env .rosetta - .network_status_or_panic() + .network_status() .await + .unwrap() .current_block_identifier - .index - + 1; - env.restart_rosetta_node(true).await; + .index; + env.restart_rosetta_node(true, true).await.unwrap(); assert_eq!( env.rosetta.status_or_panic().await.rosetta_blocks_mode, RosettaBlocksMode::Enabled { @@ -465,7 +497,7 @@ async fn test_rosetta_blocks_mode_enabled() { // Check that once rosetta blocks mode is enabled then // it will be enabled every time Rosetta restarts even // without passing --enable-rosetta-blocks - env.restart_rosetta_node(false).await; + env.restart_rosetta_node(false, true).await.unwrap(); assert_eq!( env.rosetta.status_or_panic().await.rosetta_blocks_mode, RosettaBlocksMode::Enabled { @@ -493,12 +525,9 @@ impl UnwrapCandid for WasmResult { #[tokio::test] async fn test_rosetta_blocks_enabled_after_first_block() { - let mut env = TestEnv::setup_or_panic(false).await; - - // enable rosetta blocks mode - env.restart_rosetta_node(true).await; + let mut env = TestEnv::setup(false, true).await.unwrap(); + env.restart_rosetta_node(true, true).await.unwrap(); env.pocket_ic.stop_progress().await; - env.icrc1_transfers(vec![ // create block 1 and Rosetta Block 1 TransferArg { @@ -509,7 +538,7 @@ async fn test_rosetta_blocks_enabled_after_first_block() { memo: None, amount: Nat::from(1u64), }, - // create block 2 which will go inside Rosetta Block 1 + // create block 2 which will go inside Rosetta Block 2 TransferArg { from_subaccount: None, to: Account::from(Principal::anonymous()), @@ -529,40 +558,51 @@ async fn test_rosetta_blocks_enabled_after_first_block() { let old_block0 = env .rosetta - .block_or_panic(PartialBlockIdentifier { + .block(PartialBlockIdentifier { index: Some(0), hash: None, }) - .await; - - env.rosetta.wait_or_panic_until_synced_up_to(2).await; - + .await + .unwrap() + .block + .unwrap(); + env.rosetta.wait_until_synced_up_to(1).await.unwrap(); + println!("synced up to 2"); // Enabling Rosetta Blocks Mode should not change blocks before // the first rosetta index, in this case block 0 let block0 = env .rosetta - .block_or_panic(PartialBlockIdentifier { + .block(PartialBlockIdentifier { index: Some(0), hash: None, }) - .await; + .await + .unwrap() + .block + .unwrap(); assert_eq!(old_block0, block0); // Block 1 must be a Rosetta block with 2 transactions let block1_hash = env .rosetta - .block_or_panic(PartialBlockIdentifier { + .block(PartialBlockIdentifier { index: Some(1), hash: None, }) .await + .unwrap() + .block + .unwrap() .block_identifier .hash; for (index, hash) in [(Some(1), None), (Some(1), Some(block1_hash.clone()))] { let block1 = env .rosetta - .block_or_panic(PartialBlockIdentifier { index, hash }) - .await; + .block(PartialBlockIdentifier { index, hash }) + .await + .unwrap() + .block + .unwrap(); assert_eq!(block1.block_identifier.index, 1); assert_eq!(block1.block_identifier.hash, block1_hash); assert_eq!(block1.parent_block_identifier, block0.block_identifier); @@ -620,7 +660,7 @@ async fn test_rosetta_blocks_enabled_after_first_block() { #[tokio::test] async fn test_rosetta_blocks_dont_contain_transactions_duplicates() { - let env = TestEnv::setup_or_panic(true).await; + let env = TestEnv::setup(true, true).await.unwrap(); // Rosetta block 0 contains transaction 0 env.pocket_ic.stop_progress().await; @@ -675,23 +715,29 @@ async fn test_rosetta_blocks_dont_contain_transactions_duplicates() { env.pocket_ic.auto_progress().await; - env.rosetta.wait_or_panic_until_synced_up_to(4).await; + env.rosetta.wait_until_synced_up_to(3).await.unwrap(); // check block 1 let block0 = env .rosetta - .block_or_panic(PartialBlockIdentifier { + .block(PartialBlockIdentifier { index: Some(0), hash: None, }) - .await; + .await + .unwrap() + .block + .unwrap(); let block1 = env .rosetta - .block_or_panic(PartialBlockIdentifier { + .block(PartialBlockIdentifier { index: Some(1), hash: None, }) - .await; + .await + .unwrap() + .block + .unwrap(); assert_eq!(block1.block_identifier.index, 1); assert_eq!(block1.parent_block_identifier, block0.block_identifier); assert_eq!(block1.timestamp, rosetta_block1_expected_time_millis); @@ -721,11 +767,14 @@ async fn test_rosetta_blocks_dont_contain_transactions_duplicates() { // check block 2 let block2 = env .rosetta - .block_or_panic(PartialBlockIdentifier { + .block(PartialBlockIdentifier { index: Some(2), hash: None, }) - .await; + .await + .unwrap() + .block + .unwrap(); assert_eq!(block2.block_identifier.index, 2); assert_eq!(block2.parent_block_identifier, block1.block_identifier); assert_eq!(block2.timestamp, rosetta_block1_expected_time_millis); @@ -775,11 +824,14 @@ async fn test_rosetta_blocks_dont_contain_transactions_duplicates() { // check block 3 let block3 = env .rosetta - .block_or_panic(PartialBlockIdentifier { + .block(PartialBlockIdentifier { index: Some(3), hash: None, }) - .await; + .await + .unwrap() + .block + .unwrap(); assert_eq!(block3.block_identifier.index, 3); assert_eq!(block3.parent_block_identifier, block2.block_identifier); assert_eq!(block3.timestamp, rosetta_block1_expected_time_millis); @@ -809,7 +861,7 @@ async fn test_rosetta_blocks_dont_contain_transactions_duplicates() { #[tokio::test] async fn test_query_block_range() { - let env = TestEnv::setup_or_panic(false).await; + let env = TestEnv::setup(false, true).await.unwrap(); env.pocket_ic.auto_progress().await; let minter = test_identity() @@ -837,8 +889,9 @@ async fn test_query_block_range() { } env.rosetta - .wait_or_panic_until_synced_up_to(block_indices.last().unwrap().0.to_u64().unwrap()) - .await; + .wait_until_synced_up_to(block_indices.last().unwrap().0.to_u64().unwrap()) + .await + .unwrap(); let response: QueryBlockRangeResponse = env .rosetta @@ -860,7 +913,7 @@ async fn test_query_block_range() { #[tokio::test] async fn test_block_transaction() { - let env = TestEnv::setup_or_panic(true).await; + let env = TestEnv::setup(true, true).await.unwrap(); env.pocket_ic.stop_progress().await; assert!(env .rosetta @@ -879,7 +932,7 @@ async fn test_block_transaction() { .message .contains("Block not found")); - // We are creating a second rosetta block that contains 4 transactions with each having an unique tx hash + // We are creating a second rosetta block that contains 4 transactions with each having a unique tx hash env.icrc1_transfers(vec![ TransferArg { from_subaccount: None, @@ -916,7 +969,8 @@ async fn test_block_transaction() { ]) .await; env.pocket_ic.auto_progress().await; - env.rosetta.wait_or_panic_until_synced_up_to(4).await; + // All the previous transactions are stored in a single rosetta block so we wait until rosetta block 1 is finished + env.rosetta.wait_until_synced_up_to(1).await.unwrap(); // We try to fetch the RosettaBlock we just created earlier let rosetta_core::objects::Block { @@ -925,11 +979,14 @@ async fn test_block_transaction() { .. } = env .rosetta - .block_or_panic(PartialBlockIdentifier { + .block(PartialBlockIdentifier { index: Some(1), hash: None, }) - .await; + .await + .unwrap() + .block + .unwrap(); let transaction = env .rosetta @@ -967,3 +1024,308 @@ async fn test_block_transaction() { .message .contains("Invalid transaction id")); } + +#[tokio::test] +async fn test_network_status_multiple_genesis_transactions() { + // We start off by testing the case with no rosetta blocks enabled + let mut env = TestEnv::setup(false, true).await.unwrap(); + let network_status = env.rosetta.network_status().await.unwrap(); + let genesis_block = env + .rosetta + .block(PartialBlockIdentifier { + index: Some(0), + hash: None, + }) + .await + .unwrap() + .block + .unwrap(); + + // We expect the genesis block to be present and be in the network status + assert_eq!( + network_status.current_block_identifier, + genesis_block.block_identifier + ); + assert_eq!( + network_status.current_block_timestamp, + genesis_block.timestamp + ); + assert_eq!( + network_status.genesis_block_identifier, + genesis_block.block_identifier + ); + // If only the genesis block exists than the oldest block identifier is expected to be None + assert_eq!(network_status.oldest_block_identifier, None); + + // Now we restart rosetta with rosetta blocks enabled + // We need to restart it into memory or otherwise we will trigger the rosetta block mode detection from earlier restarts + env.restart_rosetta_node(true, false).await.unwrap(); + let network_status = env.rosetta.network_status().await.unwrap(); + // There are no rosettablocks created yet so we return an empty blockchain error + let current_block = env + .rosetta + .block(PartialBlockIdentifier { + index: Some(0), + hash: None, + }) + .await + .unwrap() + .block + .unwrap(); + let genesis_block = env + .rosetta + .block(PartialBlockIdentifier { + index: Some(0), + hash: None, + }) + .await + .unwrap() + .block + .unwrap(); + assert_eq!( + network_status.current_block_identifier, + current_block.block_identifier + ); + assert_eq!( + network_status.current_block_timestamp, + current_block.timestamp + ); + assert_eq!( + network_status.genesis_block_identifier, + genesis_block.block_identifier + ); + // If only the genesis block exists than the oldest block identifier is expected to be None + assert_eq!(network_status.oldest_block_identifier, None); + + // Now we test the case where we have produced some blocks + env.restart_rosetta_node(false, false).await.unwrap(); + // After this call we should have 4 icp blocks, genesis and three transfers. The maximum block idx should be 3 + env.pocket_ic.stop_progress().await; + env.icrc1_transfers(vec![ + TransferArg { + from_subaccount: None, + to: Account::from(Principal::anonymous()), + fee: None, + created_at_time: None, + memo: None, + amount: Nat::from(1u64), + }, + TransferArg { + from_subaccount: None, + to: Account::from(Principal::anonymous()), + fee: None, + created_at_time: None, + memo: None, + amount: Nat::from(2u64), + }, + TransferArg { + from_subaccount: None, + to: Account::from(Principal::anonymous()), + fee: None, + created_at_time: None, + memo: None, + amount: Nat::from(2u64), + }, + ]) + .await; + env.pocket_ic.auto_progress().await; + env.rosetta.wait_until_synced_up_to(3).await.unwrap(); + + let network_status = env.rosetta.network_status().await.unwrap(); + let genesis_block = env + .rosetta + .block(PartialBlockIdentifier { + index: Some(0), + hash: None, + }) + .await + .unwrap() + .block + .unwrap(); + let current_block = env + .rosetta + .block(PartialBlockIdentifier { + index: Some(3), + hash: None, + }) + .await + .unwrap() + .block + .unwrap(); + assert_eq!( + network_status.current_block_identifier, + current_block.block_identifier + ); + assert_eq!( + network_status.current_block_timestamp, + current_block.timestamp + ); + assert_eq!( + network_status.genesis_block_identifier, + genesis_block.block_identifier.clone() + ); + // Genesis block is verified so there is no need for the oldest block identifier + assert_eq!(network_status.oldest_block_identifier, None); + + // If we restart rosetta now we have 3 icp blocks to sync out of which the first two will go into the rosetta block + env.restart_rosetta_node(true, false).await.unwrap(); + // Now we have 3 rosetta blocks, the genesis block and the first transfer go into rosetta block 0, the second and third transfer each go into a separate rosetta block. The maximum block idx is thus 2 + env.rosetta.wait_until_synced_up_to(2).await.unwrap(); + let network_status = env.rosetta.network_status().await.unwrap(); + let current_block = env + .rosetta + .block(PartialBlockIdentifier { + index: Some(2), + hash: None, + }) + .await + .unwrap() + .block + .unwrap(); + let genesis_block = env + .rosetta + .block(PartialBlockIdentifier { + index: Some(0), + hash: None, + }) + .await + .unwrap() + .block + .unwrap(); + assert_eq!( + network_status.current_block_identifier, + current_block.block_identifier + ); + assert_eq!( + network_status.current_block_timestamp, + current_block.timestamp + ); + assert_eq!( + network_status.genesis_block_identifier, + genesis_block.block_identifier + ); + assert_eq!(network_status.oldest_block_identifier, None); + // We should not be able to call block with block index 3 at this point + assert!(env + .rosetta + .block(PartialBlockIdentifier { + index: Some(3), + hash: None, + }) + .await + .unwrap_err() + .0 + .message + .contains("Block not found")); +} + +#[tokio::test] +async fn test_network_status_single_genesis_transaction() { + let mut env = TestEnv::setup(false, true).await.unwrap(); + let t1 = env.pocket_ic.get_time().await; + // We need to advance the time to make sure only a single transaction gets into the genesis block + env.pocket_ic.auto_progress().await; + tokio::time::sleep(Duration::from_secs(1)).await; + env.pocket_ic.stop_progress().await; + let t2 = env.pocket_ic.get_time().await; + assert!(t1 < t2); + // We want two transactions with unique tx hashes + env.icrc1_transfers(vec![ + TransferArg { + from_subaccount: None, + to: Account::from(Principal::anonymous()), + fee: None, + created_at_time: None, + memo: Some(1.into()), + amount: Nat::from(1u64), + }, + TransferArg { + from_subaccount: None, + to: Account::from(Principal::anonymous()), + fee: None, + created_at_time: None, + memo: Some(2.into()), + amount: Nat::from(2u64), + }, + ]) + .await; + env.pocket_ic.auto_progress().await; + // We should have 3 ICP blocks by now + env.rosetta.wait_until_synced_up_to(2).await.unwrap(); + let genesis_block = env + .rosetta + .block(PartialBlockIdentifier { + index: Some(0), + hash: None, + }) + .await + .unwrap() + .block + .unwrap(); + let current_block = env + .rosetta + .block(PartialBlockIdentifier { + index: Some(2), + hash: None, + }) + .await + .unwrap() + .block + .unwrap(); + let network_status = env.rosetta.network_status().await.unwrap(); + assert_eq!( + network_status.current_block_identifier, + current_block.block_identifier + ); + assert_eq!( + network_status.current_block_timestamp, + current_block.timestamp + ); + assert_eq!( + network_status.genesis_block_identifier, + genesis_block.block_identifier + ); + + // Now we restart rosetta with rosetta blocks + env.restart_rosetta_node(true, false).await.unwrap(); + // We should now have 2 Rosetta blocks, genesis block with a single transaction and a second rosetta block with two transfers + env.rosetta.wait_until_synced_up_to(1).await.unwrap(); + + // The genesis block stays the same but the current block changes + let current_block = env + .rosetta + .block(PartialBlockIdentifier { + index: Some(1), + hash: None, + }) + .await + .unwrap() + .block + .unwrap(); + + // Even though both the ICP genesis block and the Rosetta Block genesis block have only one transaction in them they have different hashes. One hashes a single transaction the other an array that contains a single transaction + let genesis_block = env + .rosetta + .block(PartialBlockIdentifier { + index: Some(0), + hash: None, + }) + .await + .unwrap() + .block + .unwrap(); + let network_status = env.rosetta.network_status().await.unwrap(); + assert_eq!( + network_status.current_block_identifier, + current_block.block_identifier + ); + assert_eq!( + network_status.current_block_timestamp, + current_block.timestamp + ); + + assert_eq!( + network_status.genesis_block_identifier, + genesis_block.block_identifier + ); +} diff --git a/rs/rosetta-api/icrc1/BUILD.bazel b/rs/rosetta-api/icrc1/BUILD.bazel index c425cbaaeef..aaa4630e482 100644 --- a/rs/rosetta-api/icrc1/BUILD.bazel +++ b/rs/rosetta-api/icrc1/BUILD.bazel @@ -133,3 +133,64 @@ rust_ic_test( "@crate_index//:candid", ], ) + +[ + rust_ic_test( + name = "upgrade_downgrade" + name_suffix, + srcs = ["tests/upgrade_downgrade.rs"], + crate_features = features, + data = [ + "//rs/rosetta-api/icrc1/index-ng:index_ng_canister" + name_suffix + ".wasm.gz", + "//rs/rosetta-api/icrc1/ledger:ledger_canister" + name_suffix + ".wasm", + "@" + mainnet_ledger + "//file", + "@" + mainnet_index + "//file", + ], + env = { + "CARGO_MANIFEST_DIR": "rs/rosetta-api/icrc1", + "IC_ICRC1_INDEX_NG_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @" + mainnet_index + "//file)", + "IC_ICRC1_INDEX_NG_WASM_PATH": "$(rootpath //rs/rosetta-api/icrc1/index-ng:index_ng_canister" + name_suffix + ".wasm.gz)", + "IC_ICRC1_LEDGER_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @" + mainnet_ledger + "//file)", + "IC_ICRC1_LEDGER_WASM_PATH": "$(rootpath //rs/rosetta-api/icrc1/ledger:ledger_canister" + name_suffix + ".wasm)", + }, + deps = [ + # Keep sorted. + "//packages/ic-ledger-hash-of:ic_ledger_hash_of", + "//packages/icrc-ledger-types:icrc_ledger_types", + "//rs/registry/subnet_type", + "//rs/rosetta-api/icrc1", + "//rs/rosetta-api/icrc1/index-ng", + "//rs/rosetta-api/icrc1/ledger", + "//rs/rosetta-api/icrc1/ledger/sm-tests:sm-tests" + name_suffix, + "//rs/rosetta-api/ledger_canister_core", + "//rs/rosetta-api/ledger_core", + "//rs/rust_canisters/dfn_http_metrics", + "//rs/state_machine_tests", + "//rs/test_utilities/load_wasm", + "//rs/types/base_types", + "@crate_index//:candid", + "@crate_index//:cddl", + "@crate_index//:hex", + "@crate_index//:ic-metrics-encoder", + "@crate_index//:leb128", + "@crate_index//:num-traits", + "@crate_index//:proptest", + "@crate_index//:serde_bytes", + ] + extra_deps, + ) + for (name_suffix, mainnet_ledger, mainnet_index, features, extra_deps) in [ + ( + "", + "mainnet_ckbtc_ic-icrc1-ledger", + "mainnet_ckbtc-index-ng", + [], + ["//rs/rosetta-api/icrc1/tokens_u64"], + ), + ( + "_u256", + "mainnet_cketh_ic-icrc1-ledger-u256", + "mainnet_cketh-index-ng", + ["u256-tokens"], + ["//rs/rosetta-api/icrc1/tokens_u256"], + ), + ] +] diff --git a/rs/rosetta-api/icrc1/Cargo.toml b/rs/rosetta-api/icrc1/Cargo.toml index e027a92681e..2c3cc68f5f7 100644 --- a/rs/rosetta-api/icrc1/Cargo.toml +++ b/rs/rosetta-api/icrc1/Cargo.toml @@ -25,11 +25,13 @@ thiserror = { workspace = true } [dev-dependencies] canister-test = { path = "../../rust_canisters/canister_test" } +ic-icrc1-index-ng = { path = "index-ng" } ic-icrc1-ledger = { path = "ledger" } ic-icrc1-ledger-sm-tests = { path = "ledger/sm-tests" } ic-icrc1-test-utils = { path = "test_utils" } ic-icrc1-tokens-u256 = { path = "tokens_u256" } ic-icrc1-tokens-u64 = { path = "tokens_u64" } +ic-registry-subnet-type = { path = "../../registry/subnet_type" } ic-nns-test-utils = { path = "../../nns/test_utils" } ic-nns-test-utils-golden-nns-state = { path = "../../nns/test_utils/golden_nns_state" } ic-state-machine-tests = { path = "../../state_machine_tests" } diff --git a/rs/rosetta-api/icrc1/ledger/sm-tests/BUILD.bazel b/rs/rosetta-api/icrc1/ledger/sm-tests/BUILD.bazel index c1b22aa0b6b..f90be116f6a 100644 --- a/rs/rosetta-api/icrc1/ledger/sm-tests/BUILD.bazel +++ b/rs/rosetta-api/icrc1/ledger/sm-tests/BUILD.bazel @@ -1,7 +1,39 @@ -load("@rules_rust//rust:defs.bzl", "rust_library") +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") package(default_visibility = ["//visibility:public"]) +DEPENDENCIES = [ + # Keep sorted. + "//packages/ic-ledger-hash-of:ic_ledger_hash_of", + "//packages/icrc-ledger-types:icrc_ledger_types", + "//rs/rosetta-api/icrc1", + "//rs/rosetta-api/icrc1/ledger", + "//rs/rosetta-api/ledger_canister_core", + "//rs/rosetta-api/ledger_core", + "//rs/rust_canisters/http_types", + "//rs/state_machine_tests", + "//rs/types/base_types", + "//rs/types/error_types", + "//rs/types/management_canister_types", + "//rs/types/types", + "//rs/universal_canister/lib", + "@crate_index//:anyhow", + "@crate_index//:candid", + "@crate_index//:cddl", + "@crate_index//:futures", + "@crate_index//:hex", + "@crate_index//:icrc1-test-env", + "@crate_index//:icrc1-test-suite", + "@crate_index//:num-traits", + "@crate_index//:proptest", + "@crate_index//:serde", +] + +MACRO_DEPENDENCIES = [ + # Keep sorted. + "@crate_index//:async-trait", +] + [ rust_library( name = "sm-tests" + name_suffix, @@ -14,37 +46,30 @@ package(default_visibility = ["//visibility:public"]) data = [ "//rs/rosetta-api/icrc1/ledger:block.cddl", ], - proc_macro_deps = [ - # Keep sorted. - "@crate_index//:async-trait", - ], + proc_macro_deps = MACRO_DEPENDENCIES, version = "0.9.0", - deps = [ - # Keep sorted. - "//packages/ic-ledger-hash-of:ic_ledger_hash_of", - "//packages/icrc-ledger-types:icrc_ledger_types", - "//rs/rosetta-api/icrc1", - "//rs/rosetta-api/icrc1/ledger", - "//rs/rosetta-api/ledger_canister_core", - "//rs/rosetta-api/ledger_core", - "//rs/rust_canisters/http_types", - "//rs/state_machine_tests", - "//rs/types/base_types", - "//rs/types/error_types", - "//rs/types/management_canister_types", - "//rs/types/types", - "//rs/universal_canister/lib", - "@crate_index//:anyhow", - "@crate_index//:candid", - "@crate_index//:cddl", - "@crate_index//:futures", - "@crate_index//:hex", - "@crate_index//:icrc1-test-env", - "@crate_index//:icrc1-test-suite", - "@crate_index//:num-traits", - "@crate_index//:proptest", - "@crate_index//:serde", - ] + extra_deps, + deps = DEPENDENCIES + extra_deps, + ) + for (name_suffix, features, extra_deps) in [ + ( + "", + [], + ["//rs/rosetta-api/icrc1/tokens_u64"], + ), + ( + "_u256", + ["u256-tokens"], + ["//rs/rosetta-api/icrc1/tokens_u256"], + ), + ] +] + +[ + rust_test( + name = "sm-tests-unit-tests" + name_suffix, + crate = ":sm-tests" + name_suffix, + crate_features = features, + deps = DEPENDENCIES + extra_deps, ) for (name_suffix, features, extra_deps) in [ ( diff --git a/rs/rosetta-api/icrc1/ledger/sm-tests/src/in_memory_ledger.rs b/rs/rosetta-api/icrc1/ledger/sm-tests/src/in_memory_ledger.rs new file mode 100644 index 00000000000..f5a31cda716 --- /dev/null +++ b/rs/rosetta-api/icrc1/ledger/sm-tests/src/in_memory_ledger.rs @@ -0,0 +1,437 @@ +use super::{get_all_ledger_and_archive_blocks, get_allowance, Tokens}; +use crate::metrics::parse_metric; +use candid::{Decode, Encode, Nat}; +use ic_base_types::CanisterId; +use ic_icrc1::Operation; +use ic_ledger_core::approvals::Allowance; +use ic_ledger_core::timestamp::TimeStamp; +use ic_ledger_core::tokens::{TokensType, Zero}; +use ic_state_machine_tests::StateMachine; +use icrc_ledger_types::icrc1::account::Account; +use std::collections::HashMap; +use std::hash::Hash; + +#[cfg(test)] +mod tests; + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub struct ApprovalKey(Account, Account); + +impl From<(&Account, &Account)> for ApprovalKey { + fn from((account, spender): (&Account, &Account)) -> Self { + Self(*account, *spender) + } +} + +impl From for (Account, Account) { + fn from(key: ApprovalKey) -> Self { + (key.0, key.1) + } +} + +trait InMemoryLedgerState { + type AccountId; + type Tokens; + + fn process_approve( + &mut self, + from: &Self::AccountId, + spender: &Self::AccountId, + amount: &Self::Tokens, + expected_allowance: &Option, + expires_at: &Option, + fee: &Option, + now: TimeStamp, + ); + fn process_burn( + &mut self, + from: &Self::AccountId, + spender: &Option, + amount: &Self::Tokens, + ); + fn process_mint(&mut self, to: &Self::AccountId, amount: &Self::Tokens); + fn process_transfer( + &mut self, + from: &Self::AccountId, + to: &Self::AccountId, + spender: &Option, + amount: &Self::Tokens, + fee: &Option, + ); + fn validate_invariants(&self); +} + +pub struct InMemoryLedger +where + K: Ord, +{ + pub balances: HashMap, + pub allowances: HashMap>, + pub total_supply: Tokens, + pub fee_collector: Option, +} + +impl InMemoryLedgerState for InMemoryLedger +where + K: Ord + for<'a> From<(&'a AccountId, &'a AccountId)> + Clone + Hash, + K: Into<(AccountId, AccountId)>, + AccountId: PartialEq + Ord + Clone + Hash + std::fmt::Debug, + Tokens: TokensType + Default, +{ + type AccountId = AccountId; + type Tokens = Tokens; + + fn process_approve( + &mut self, + from: &Self::AccountId, + spender: &Self::AccountId, + amount: &Self::Tokens, + expected_allowance: &Option, + expires_at: &Option, + fee: &Option, + now: TimeStamp, + ) { + self.burn_fee(from, fee); + self.set_allowance(from, spender, amount, expected_allowance, expires_at, now); + } + + fn process_burn( + &mut self, + from: &Self::AccountId, + spender: &Option, + amount: &Self::Tokens, + ) { + self.decrease_balance(from, amount); + self.decrease_total_supply(amount); + if let Some(spender) = spender { + if from != spender { + self.decrease_allowance(from, spender, amount, None); + } + } + } + + fn process_mint(&mut self, to: &Self::AccountId, amount: &Self::Tokens) { + self.increase_balance(to, amount); + self.increase_total_supply(amount); + } + + fn process_transfer( + &mut self, + from: &Self::AccountId, + to: &Self::AccountId, + spender: &Option, + amount: &Self::Tokens, + fee: &Option, + ) { + self.decrease_balance(from, amount); + self.collect_fee(from, fee); + if let Some(fee) = fee { + if let Some(spender) = spender { + if from != spender { + self.decrease_allowance(from, spender, amount, Some(fee)); + } + } + } + self.increase_balance(to, amount); + } + + fn validate_invariants(&self) { + let mut balances_total = Self::Tokens::default(); + for amount in self.balances.values() { + balances_total = balances_total.checked_add(amount).unwrap(); + assert_ne!(amount, &Tokens::zero()); + } + assert_eq!(self.total_supply, balances_total); + for allowance in self.allowances.values() { + assert_ne!(&allowance.amount, &Tokens::zero()); + } + } +} + +impl Default for InMemoryLedger +where + K: Ord + for<'a> From<(&'a AccountId, &'a AccountId)> + Clone + Hash, + K: Into<(AccountId, AccountId)>, + AccountId: PartialEq + Ord + Clone + Hash, + Tokens: TokensType, +{ + fn default() -> Self { + InMemoryLedger { + balances: HashMap::new(), + allowances: HashMap::new(), + total_supply: Tokens::zero(), + fee_collector: None, + } + } +} + +impl InMemoryLedger +where + K: Ord + for<'a> From<(&'a AccountId, &'a AccountId)> + Clone + Hash, + K: Into<(AccountId, AccountId)>, + AccountId: PartialEq + Ord + Clone + Hash, + Tokens: TokensType, +{ + fn decrease_allowance( + &mut self, + from: &AccountId, + spender: &AccountId, + amount: &Tokens, + fee: Option<&Tokens>, + ) { + let key = K::from((from, spender)); + let old_allowance = self + .allowances + .get(&key) + .unwrap_or_else(|| panic!("Allowance not found",)); + let mut new_allowance_value = old_allowance + .amount + .checked_sub(amount) + .unwrap_or_else(|| panic!("Insufficient allowance",)); + if let Some(fee) = fee { + new_allowance_value = new_allowance_value + .checked_sub(fee) + .unwrap_or_else(|| panic!("Insufficient allowance",)); + } + if new_allowance_value.is_zero() { + self.allowances.remove(&key); + } else { + self.allowances.insert( + key, + Allowance { + amount: new_allowance_value, + expires_at: old_allowance.expires_at, + arrived_at: old_allowance.arrived_at, + }, + ); + } + } + + fn decrease_balance(&mut self, from: &AccountId, amount: &Tokens) { + let old_balance = self + .balances + .get(from) + .unwrap_or_else(|| panic!("Account not found",)); + let new_balance = old_balance + .checked_sub(amount) + .unwrap_or_else(|| panic!("Insufficient balance",)); + if new_balance.is_zero() { + self.balances.remove(from); + } else { + self.balances.insert(from.clone(), new_balance); + } + } + + fn decrease_total_supply(&mut self, amount: &Tokens) { + self.total_supply = self + .total_supply + .checked_sub(amount) + .unwrap_or_else(|| panic!("Total supply underflow",)); + } + + fn set_allowance( + &mut self, + from: &AccountId, + spender: &AccountId, + amount: &Tokens, + expected_allowance: &Option, + expires_at: &Option, + arrived_at: TimeStamp, + ) { + let key = K::from((from, spender)); + if let Some(expected_allowance) = expected_allowance { + let current_allowance = self + .allowances + .get(&key) + .unwrap_or_else(|| panic!("No current allowance but expected allowance set")); + if current_allowance.amount != *expected_allowance { + panic!("Expected allowance does not match current allowance"); + } + } + if amount == &Tokens::zero() { + self.allowances.remove(&key); + } else { + self.allowances.insert( + key, + Allowance { + amount: amount.clone(), + expires_at: expires_at.map(TimeStamp::from_nanos_since_unix_epoch), + arrived_at, + }, + ); + } + } + + fn increase_balance(&mut self, to: &AccountId, amount: &Tokens) { + let new_balance = match self.balances.get(to) { + None => amount.clone(), + Some(old_balance) => old_balance + .checked_add(amount) + .unwrap_or_else(|| panic!("Balance overflow")), + }; + if !new_balance.is_zero() { + self.balances.insert(to.clone(), new_balance); + } + } + + fn increase_total_supply(&mut self, amount: &Tokens) { + self.total_supply = self + .total_supply + .checked_add(amount) + .unwrap_or_else(|| panic!("Total supply overflow")); + } + + fn collect_fee(&mut self, from: &AccountId, amount: &Option) { + if let Some(amount) = amount { + self.decrease_balance(from, amount); + if let Some(fee_collector) = &self.fee_collector { + self.increase_balance(&fee_collector.clone(), amount); + } else { + self.decrease_total_supply(amount); + } + } + } + + fn burn_fee(&mut self, from: &AccountId, amount: &Option) { + if let Some(amount) = amount { + self.decrease_balance(from, amount); + self.decrease_total_supply(amount); + } + } + + fn prune_expired_allowances(&mut self, now: TimeStamp) { + let expired_allowances: Vec = self + .allowances + .iter() + .filter_map(|(key, allowance)| { + if let Some(expires_at) = allowance.expires_at { + if now >= expires_at { + return Some(key.clone()); + } + } + None + }) + .collect(); + for key in expired_allowances { + self.allowances.remove(&key); + } + } +} + +impl InMemoryLedger { + fn new_from_icrc1_ledger_blocks( + blocks: &[ic_icrc1::Block], + ) -> InMemoryLedger { + let mut state = InMemoryLedger::default(); + for block in blocks { + if let Some(fee_collector) = block.fee_collector { + state.fee_collector = Some(fee_collector); + } + match &block.transaction.operation { + Operation::Mint { to, amount } => state.process_mint(to, amount), + Operation::Transfer { + from, + to, + spender, + amount, + fee, + } => { + state.process_transfer(from, to, spender, amount, &fee.or(block.effective_fee)) + } + Operation::Burn { + from, + spender, + amount, + } => state.process_burn(from, spender, amount), + Operation::Approve { + from, + spender, + amount, + expected_allowance, + expires_at, + fee, + } => state.process_approve( + from, + spender, + amount, + expected_allowance, + expires_at, + &fee.or(block.effective_fee), + TimeStamp::from_nanos_since_unix_epoch(block.timestamp), + ), + } + state.validate_invariants(); + } + state.prune_expired_allowances(TimeStamp::from_nanos_since_unix_epoch( + blocks.last().unwrap().timestamp, + )); + state + } +} + +pub fn verify_ledger_state(env: &StateMachine, ledger_id: CanisterId) { + println!("verifying state of ledger {}", ledger_id); + let blocks = get_all_ledger_and_archive_blocks(env, ledger_id); + println!("retrieved all ledger and archive blocks"); + let expected_ledger_state = InMemoryLedger::new_from_icrc1_ledger_blocks(&blocks); + println!("recreated expected ledger state"); + let actual_num_approvals = parse_metric(env, ledger_id, "ledger_num_approvals"); + let actual_num_balances = parse_metric(env, ledger_id, "ledger_balance_store_entries"); + assert_eq!( + expected_ledger_state.balances.len() as u64, + actual_num_balances, + "Mismatch in number of balances ({} vs {})", + expected_ledger_state.balances.len(), + actual_num_balances + ); + assert_eq!( + expected_ledger_state.allowances.len() as u64, + actual_num_approvals, + "Mismatch in number of approvals ({} vs {})", + expected_ledger_state.allowances.len(), + actual_num_approvals + ); + println!( + "Checking {} balances and {} allowances", + actual_num_balances, actual_num_approvals + ); + for (account, balance) in expected_ledger_state.balances.iter() { + let actual_balance = Decode!( + &env.query(ledger_id, "icrc1_balance_of", Encode!(account).unwrap()) + .expect("failed to query balance") + .bytes(), + Nat + ) + .expect("failed to decode balance_of response"); + + assert_eq!( + &Tokens::try_from(actual_balance.clone()).unwrap(), + balance, + "Mismatch in balance for account {:?} ({} vs {})", + account, + balance, + actual_balance + ); + } + for (approval, allowance) in expected_ledger_state.allowances.iter() { + let (from, spender): (Account, Account) = approval.clone().into(); + assert!( + !allowance.amount.is_zero(), + "Expected allowance is zero! Should not happen... from: {:?}, spender: {:?}", + &from, + &spender + ); + let actual_allowance = get_allowance(env, ledger_id, from, spender); + assert_eq!( + allowance.amount, + Tokens::try_from(actual_allowance.allowance.clone()).unwrap(), + "Mismatch in allowance for approval from {:?} spender {:?}: {:?} ({:?} vs {:?})", + &from, + &spender, + approval, + allowance, + actual_allowance + ); + } + println!("ledger state verified successfully"); +} diff --git a/rs/rosetta-api/icrc1/ledger/sm-tests/src/in_memory_ledger/tests.rs b/rs/rosetta-api/icrc1/ledger/sm-tests/src/in_memory_ledger/tests.rs new file mode 100644 index 00000000000..5456f92853d --- /dev/null +++ b/rs/rosetta-api/icrc1/ledger/sm-tests/src/in_memory_ledger/tests.rs @@ -0,0 +1,419 @@ +use crate::in_memory_ledger::{ApprovalKey, InMemoryLedger, InMemoryLedgerState, Tokens}; +use ic_ledger_core::approvals::Allowance; +use ic_ledger_core::timestamp::TimeStamp; +use ic_ledger_core::tokens::{CheckedAdd, CheckedSub}; +use ic_types::PrincipalId; +use icrc_ledger_types::icrc1::account::Account; + +const ACCOUNT_ID_1: u64 = 134; +const ACCOUNT_ID_2: u64 = 256; +const ACCOUNT_ID_3: u64 = 378; +const MINT_AMOUNT: u64 = 1_000_000u64; +const BURN_AMOUNT: u64 = 500_000u64; +const TRANSFER_AMOUNT: u64 = 200_000u64; +const APPROVE_AMOUNT: u64 = 250_000u64; +const ANOTHER_APPROVE_AMOUNT: u64 = 700_000u64; +const ZERO_AMOUNT: u64 = 0u64; +const FEE_AMOUNT: u64 = 10_000u64; +const TIMESTAMP_NOW: u64 = 0; +const TIMESTAMP_LATER: u64 = 1; + +struct LedgerBuilder { + ledger: InMemoryLedger, +} + +impl LedgerBuilder { + fn new() -> Self { + Self { + ledger: InMemoryLedger::default(), + } + } + + fn with_mint(mut self, to: &Account, amount: &Tokens) -> Self { + self.ledger.process_mint(to, amount); + self.ledger.validate_invariants(); + self + } + + fn with_burn(mut self, from: &Account, spender: &Option, amount: &Tokens) -> Self { + self.ledger.process_burn(from, spender, amount); + self.ledger.validate_invariants(); + self + } + + fn with_transfer( + mut self, + from: &Account, + to: &Account, + spender: &Option, + amount: &Tokens, + fee: &Option, + ) -> Self { + self.ledger.process_transfer(from, to, spender, amount, fee); + self.ledger.validate_invariants(); + self + } + + fn with_approve( + mut self, + from: &Account, + spender: &Account, + amount: &Tokens, + expected_allowance: &Option, + expires_at: &Option, + fee: &Option, + now: TimeStamp, + ) -> Self { + self.ledger.process_approve( + from, + spender, + amount, + expected_allowance, + expires_at, + fee, + now, + ); + self.ledger.validate_invariants(); + self + } + + fn build(self) -> InMemoryLedger { + self.ledger + } +} + +#[test] +fn should_increase_balance_and_total_supply_with_mint() { + let ledger = LedgerBuilder::new() + .with_mint(&account_from_u64(ACCOUNT_ID_1), &Tokens::from(MINT_AMOUNT)) + .build(); + + assert_eq!(ledger.balances.len(), 1); + assert!(ledger.allowances.is_empty()); + assert_eq!(ledger.total_supply, Tokens::from(MINT_AMOUNT)); + let actual_balance = ledger.balances.get(&account_from_u64(ACCOUNT_ID_1)); + assert_eq!(Some(&Tokens::from(MINT_AMOUNT)), actual_balance); + assert_eq!(ledger.total_supply, Tokens::from(MINT_AMOUNT)); +} + +#[test] +fn should_decrease_balance_with_burn() { + let ledger = LedgerBuilder::new() + .with_mint(&account_from_u64(ACCOUNT_ID_1), &Tokens::from(MINT_AMOUNT)) + .with_burn( + &account_from_u64(ACCOUNT_ID_1), + &None, + &Tokens::from(BURN_AMOUNT), + ) + .build(); + + let expected_balance = Tokens::from(MINT_AMOUNT) + .checked_sub(&Tokens::from(BURN_AMOUNT)) + .unwrap(); + + assert_eq!(ledger.total_supply, expected_balance); + assert_eq!(ledger.balances.len(), 1); + assert!(ledger.allowances.is_empty()); + let actual_balance = ledger.balances.get(&account_from_u64(ACCOUNT_ID_1)); + assert_eq!(Some(&expected_balance), actual_balance); +} + +#[test] +fn should_remove_balance_with_burn() { + let ledger = LedgerBuilder::new() + .with_mint(&account_from_u64(ACCOUNT_ID_1), &Tokens::from(MINT_AMOUNT)) + .with_burn( + &account_from_u64(ACCOUNT_ID_1), + &None, + &Tokens::from(MINT_AMOUNT), + ) + .build(); + + assert_eq!(&ledger.total_supply, &Tokens::from(ZERO_AMOUNT)); + assert!(ledger.balances.is_empty()); + assert!(ledger.allowances.is_empty()); + let actual_balance = ledger.balances.get(&account_from_u64(ACCOUNT_ID_1)); + assert_eq!(None, actual_balance); +} + +#[test] +fn should_increase_and_decrease_balance_with_transfer() { + let ledger = LedgerBuilder::new() + .with_mint(&account_from_u64(ACCOUNT_ID_1), &Tokens::from(MINT_AMOUNT)) + .with_transfer( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + &None, + &Tokens::from(TRANSFER_AMOUNT), + &Some(Tokens::from(FEE_AMOUNT)), + ) + .build(); + + let expected_balance1 = Tokens::from(MINT_AMOUNT) + .checked_sub(&Tokens::from(TRANSFER_AMOUNT)) + .unwrap() + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap(); + + assert_eq!( + ledger.total_supply, + Tokens::from(MINT_AMOUNT) + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap() + ); + assert_eq!(ledger.balances.len(), 2); + assert!(ledger.allowances.is_empty()); + let actual_balance1 = ledger.balances.get(&account_from_u64(ACCOUNT_ID_1)); + assert_eq!(Some(&expected_balance1), actual_balance1); + let actual_balance2 = ledger.balances.get(&account_from_u64(ACCOUNT_ID_2)); + assert_eq!(Some(&Tokens::from(TRANSFER_AMOUNT)), actual_balance2); +} + +#[test] +fn should_remove_balances_with_transfer() { + let ledger = LedgerBuilder::new() + .with_mint(&account_from_u64(ACCOUNT_ID_1), &Tokens::from(FEE_AMOUNT)) + .with_transfer( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + &None, + &Tokens::from(ZERO_AMOUNT), + &Some(Tokens::from(FEE_AMOUNT)), + ) + .build(); + + assert_eq!(ledger.total_supply, Tokens::from(ZERO_AMOUNT)); + assert!(ledger.balances.is_empty()); +} + +#[test] +fn should_increase_allowance_with_approve() { + let now = TimeStamp::from_nanos_since_unix_epoch(TIMESTAMP_NOW); + let ledger = LedgerBuilder::new() + .with_mint(&account_from_u64(ACCOUNT_ID_1), &Tokens::from(MINT_AMOUNT)) + .with_approve( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + &Tokens::from(APPROVE_AMOUNT), + &None, + &None, + &Some(Tokens::from(FEE_AMOUNT)), + now, + ) + .build(); + + let expected_balance1 = Tokens::from(MINT_AMOUNT) + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap(); + assert_eq!(ledger.total_supply, expected_balance1); + assert_eq!(ledger.balances.len(), 1); + assert_eq!(ledger.allowances.len(), 1); + let actual_balance1 = ledger.balances.get(&account_from_u64(ACCOUNT_ID_1)); + assert_eq!(Some(&expected_balance1), actual_balance1); + let allowance_key = ApprovalKey::from(( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + )); + let account2_allowance = ledger.allowances.get(&allowance_key); + let expected_allowance2: Allowance = Allowance { + amount: Tokens::from(APPROVE_AMOUNT), + expires_at: None, + arrived_at: now, + }; + assert_eq!(account2_allowance, Some(&expected_allowance2)); +} + +#[test] +fn should_reset_allowance_with_second_approve() { + let now = TimeStamp::from_nanos_since_unix_epoch(TIMESTAMP_NOW); + let later = TimeStamp::from_nanos_since_unix_epoch(TIMESTAMP_LATER); + let ledger = LedgerBuilder::new() + .with_mint(&account_from_u64(ACCOUNT_ID_1), &Tokens::from(MINT_AMOUNT)) + .with_approve( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + &Tokens::from(APPROVE_AMOUNT), + &None, + &None, + &Some(Tokens::from(FEE_AMOUNT)), + now, + ) + .with_approve( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + &Tokens::from(ANOTHER_APPROVE_AMOUNT), + &None, + &None, + &Some(Tokens::from(FEE_AMOUNT)), + later, + ) + .build(); + + let expected_balance1 = Tokens::from(MINT_AMOUNT) + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap() + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap(); + assert_eq!(ledger.total_supply, expected_balance1); + assert_eq!(ledger.balances.len(), 1); + assert_eq!(ledger.allowances.len(), 1); + let actual_balance1 = ledger.balances.get(&account_from_u64(ACCOUNT_ID_1)); + assert_eq!(Some(&expected_balance1), actual_balance1); + let allowance_key = ApprovalKey::from(( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + )); + let account2_allowance = ledger.allowances.get(&allowance_key); + let expected_allowance2: Allowance = Allowance { + amount: Tokens::from(ANOTHER_APPROVE_AMOUNT), + expires_at: None, + arrived_at: later, + }; + assert_eq!(account2_allowance, Some(&expected_allowance2)); +} + +#[test] +fn should_remove_allowance_when_set_to_zero() { + let now = TimeStamp::from_nanos_since_unix_epoch(TIMESTAMP_NOW); + let later = TimeStamp::from_nanos_since_unix_epoch(TIMESTAMP_LATER); + let ledger = LedgerBuilder::new() + .with_mint(&account_from_u64(ACCOUNT_ID_1), &Tokens::from(MINT_AMOUNT)) + .with_approve( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + &Tokens::from(APPROVE_AMOUNT), + &None, + &None, + &Some(Tokens::from(FEE_AMOUNT)), + now, + ) + .with_approve( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + &Tokens::from(ZERO_AMOUNT), + &None, + &None, + &Some(Tokens::from(FEE_AMOUNT)), + later, + ) + .build(); + + let expected_balance1 = Tokens::from(MINT_AMOUNT) + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap() + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap(); + assert_eq!(ledger.total_supply, expected_balance1); + assert_eq!(ledger.balances.len(), 1); + assert!(ledger.allowances.is_empty()); + let actual_balance1 = ledger.balances.get(&account_from_u64(ACCOUNT_ID_1)); + assert_eq!(Some(&expected_balance1), actual_balance1); + let allowance_key = ApprovalKey::from(( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + )); + let account2_allowance = ledger.allowances.get(&allowance_key); + assert_eq!(account2_allowance, None); +} + +#[test] +fn should_remove_allowance_when_used_up() { + let now = TimeStamp::from_nanos_since_unix_epoch(TIMESTAMP_NOW); + let ledger = LedgerBuilder::new() + .with_mint(&account_from_u64(ACCOUNT_ID_1), &Tokens::from(MINT_AMOUNT)) + .with_approve( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + &Tokens::from(APPROVE_AMOUNT) + .checked_add(&Tokens::from(FEE_AMOUNT)) + .unwrap(), + &None, + &None, + &Some(Tokens::from(FEE_AMOUNT)), + now, + ) + .with_transfer( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_3), + &Some(account_from_u64(ACCOUNT_ID_2)), + &Tokens::from(APPROVE_AMOUNT), + &Some(Tokens::from(FEE_AMOUNT)), + ) + .build(); + + let expected_total_supply = Tokens::from(MINT_AMOUNT) + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap() + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap(); + let expected_balance1 = expected_total_supply + .checked_sub(&Tokens::from(APPROVE_AMOUNT)) + .unwrap(); + assert_eq!(ledger.total_supply, expected_total_supply); + assert_eq!(ledger.balances.len(), 2); + assert!(ledger.allowances.is_empty()); + let actual_balance1 = ledger.balances.get(&account_from_u64(ACCOUNT_ID_1)); + assert_eq!(Some(&expected_balance1), actual_balance1); + let actual_balance3 = ledger.balances.get(&account_from_u64(ACCOUNT_ID_3)); + assert_eq!(Some(&Tokens::from(APPROVE_AMOUNT)), actual_balance3); + let allowance_key = ApprovalKey::from(( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + )); + let account2_allowance = ledger.allowances.get(&allowance_key); + assert_eq!(account2_allowance, None); +} + +#[test] +fn should_increase_and_decrease_balance_with_transfer_from() { + let now = TimeStamp::from_nanos_since_unix_epoch(TIMESTAMP_NOW); + let ledger = LedgerBuilder::new() + .with_mint(&account_from_u64(ACCOUNT_ID_1), &Tokens::from(MINT_AMOUNT)) + .with_approve( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_2), + &Tokens::from(APPROVE_AMOUNT), + &None, + &None, + &Some(Tokens::from(FEE_AMOUNT)), + now, + ) + .with_transfer( + &account_from_u64(ACCOUNT_ID_1), + &account_from_u64(ACCOUNT_ID_3), + &Some(account_from_u64(ACCOUNT_ID_2)), + &Tokens::from(TRANSFER_AMOUNT), + &Some(Tokens::from(FEE_AMOUNT)), + ) + .build(); + + let expected_balance1 = Tokens::from(MINT_AMOUNT) + .checked_sub(&Tokens::from(TRANSFER_AMOUNT)) + .unwrap() + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap() + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap(); + + assert_eq!( + ledger.total_supply, + Tokens::from(MINT_AMOUNT) + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap() + .checked_sub(&Tokens::from(FEE_AMOUNT)) + .unwrap() + ); + assert_eq!(ledger.balances.len(), 2); + assert_eq!(ledger.allowances.len(), 1); + let actual_balance1 = ledger.balances.get(&account_from_u64(ACCOUNT_ID_1)); + assert_eq!(Some(&expected_balance1), actual_balance1); + let actual_balance3 = ledger.balances.get(&account_from_u64(ACCOUNT_ID_3)); + assert_eq!(Some(&Tokens::from(TRANSFER_AMOUNT)), actual_balance3); +} + +fn account_from_u64(account_id: u64) -> Account { + Account { + owner: PrincipalId::new_user_test_id(account_id).0, + subaccount: None, + } +} diff --git a/rs/rosetta-api/icrc1/ledger/sm-tests/src/lib.rs b/rs/rosetta-api/icrc1/ledger/sm-tests/src/lib.rs index 367f672fe1b..41de7877b0a 100644 --- a/rs/rosetta-api/icrc1/ledger/sm-tests/src/lib.rs +++ b/rs/rosetta-api/icrc1/ledger/sm-tests/src/lib.rs @@ -28,9 +28,9 @@ use icrc_ledger_types::icrc21::requests::{ use icrc_ledger_types::icrc21::responses::{ConsentInfo, ConsentMessage}; use icrc_ledger_types::icrc3; use icrc_ledger_types::icrc3::archive::ArchiveInfo; -use icrc_ledger_types::icrc3::blocks::BlockRange; -use icrc_ledger_types::icrc3::blocks::GenericBlock as IcrcBlock; -use icrc_ledger_types::icrc3::blocks::GetBlocksResponse; +use icrc_ledger_types::icrc3::blocks::{ + BlockRange, GenericBlock as IcrcBlock, GetBlocksRequest, GetBlocksResponse, +}; use icrc_ledger_types::icrc3::transactions::GetTransactionsRequest; use icrc_ledger_types::icrc3::transactions::GetTransactionsResponse; use icrc_ledger_types::icrc3::transactions::Transaction as Tx; @@ -45,6 +45,7 @@ use std::{ time::{Duration, SystemTime}, }; +pub mod in_memory_ledger; pub mod metrics; pub const FEE: u64 = 10_000; @@ -279,6 +280,66 @@ fn icrc21_consent_message( .expect("failed to decode icrc21_canister_call_consent_message response") } +pub fn get_all_ledger_and_archive_blocks( + state_machine: &StateMachine, + ledger_id: CanisterId, +) -> Vec> { + let req = GetBlocksRequest { + start: icrc_ledger_types::icrc1::transfer::BlockIndex::from(0u64), + length: Nat::from(u32::MAX), + }; + let req = Encode!(&req).expect("Failed to encode GetBlocksRequest"); + let res = state_machine + .query(ledger_id, "get_blocks", req) + .expect("Failed to send get_blocks request") + .bytes(); + let res = Decode!(&res, GetBlocksResponse).expect("Failed to decode GetBlocksResponse"); + // Assume that all blocks in the ledger can be retrieved in a single call. This should hold for + // most tests. + let blocks_in_ledger = res + .chain_length + .saturating_sub(res.first_index.0.to_u64().unwrap()); + assert!( + blocks_in_ledger <= res.blocks.len() as u64, + "Chain length: {}, first block index: {}, retrieved blocks: {}", + res.chain_length, + res.first_index, + res.blocks.len() + ); + let mut blocks = vec![]; + for archived in res.archived_blocks { + let mut remaining = archived.length.clone(); + let mut next_archived_txid = archived.start.clone(); + while remaining > 0u32 { + let req = GetTransactionsRequest { + start: next_archived_txid.clone(), + length: remaining.clone(), + }; + let req = + Encode!(&req).expect("Failed to encode GetTransactionsRequest for archive node"); + let canister_id = archived.callback.canister_id; + let res = state_machine + .query( + CanisterId::unchecked_from_principal(PrincipalId(canister_id)), + archived.callback.method.clone(), + req, + ) + .expect("Failed to send get_blocks request to archive") + .bytes(); + let res = Decode!(&res, BlockRange).unwrap(); + next_archived_txid += res.blocks.len() as u64; + remaining -= res.blocks.len() as u32; + blocks.extend(res.blocks); + } + } + blocks.extend(res.blocks); + blocks + .into_iter() + .map(ic_icrc1::Block::try_from) + .collect::>, String>>() + .expect("should convert generic blocks to ICRC1 blocks") +} + fn get_archive_remaining_capacity(env: &StateMachine, archive: Principal) -> u64 { let canister_id = CanisterId::unchecked_from_principal(archive.into()); Decode!( diff --git a/rs/rosetta-api/icrc1/ledger/sm-tests/src/metrics.rs b/rs/rosetta-api/icrc1/ledger/sm-tests/src/metrics.rs index 457a7d0132b..8dfb8d08acb 100644 --- a/rs/rosetta-api/icrc1/ledger/sm-tests/src/metrics.rs +++ b/rs/rosetta-api/icrc1/ledger/sm-tests/src/metrics.rs @@ -169,7 +169,7 @@ fn assert_existence_of_metric(env: &StateMachine, canister_id: CanisterId, metri ); } -fn parse_metric(env: &StateMachine, canister_id: CanisterId, metric: &str) -> u64 { +pub(crate) fn parse_metric(env: &StateMachine, canister_id: CanisterId, metric: &str) -> u64 { let metrics = retrieve_metrics(env, canister_id); for line in &metrics { let tokens: Vec<&str> = line.split(' ').collect(); diff --git a/rs/rosetta-api/icrc1/ledger/tests/tests.rs b/rs/rosetta-api/icrc1/ledger/tests/tests.rs index d172700b177..3376e4e37fc 100644 --- a/rs/rosetta-api/icrc1/ledger/tests/tests.rs +++ b/rs/rosetta-api/icrc1/ledger/tests/tests.rs @@ -2,6 +2,7 @@ use candid::{CandidType, Decode, Encode, Nat}; use ic_base_types::{CanisterId, PrincipalId}; use ic_icrc1::{Block, Operation, Transaction}; use ic_icrc1_ledger::{ChangeFeeCollector, FeatureFlags, InitArgs, LedgerArgument}; +use ic_icrc1_ledger_sm_tests::in_memory_ledger::verify_ledger_state; use ic_icrc1_ledger_sm_tests::{ get_allowance, send_approval, send_transfer_from, ARCHIVE_TRIGGER_THRESHOLD, BLOB_META_KEY, BLOB_META_VALUE, DECIMAL_PLACES, FEE, INT_META_KEY, INT_META_VALUE, MINTER, NAT_META_KEY, @@ -1047,6 +1048,8 @@ fn test_icrc3_get_blocks() { // multiple ranges check_icrc3_get_blocks(vec![(2, 3), (1, 2), (0, 10), (10, 5)]); + + verify_ledger_state(&env, ledger_id); } #[test] diff --git a/rs/rosetta-api/icrc1/tests/upgrade_downgrade.rs b/rs/rosetta-api/icrc1/tests/upgrade_downgrade.rs new file mode 100644 index 00000000000..6b8f6696356 --- /dev/null +++ b/rs/rosetta-api/icrc1/tests/upgrade_downgrade.rs @@ -0,0 +1,169 @@ +use candid::{Encode, Principal}; +use ic_base_types::{CanisterId, PrincipalId}; +use ic_icrc1_index_ng::{IndexArg, InitArg as IndexInitArg, UpgradeArg as IndexUpgradeArg}; +use ic_icrc1_ledger::{FeatureFlags, InitArgsBuilder, LedgerArgument}; +use ic_icrc1_ledger_sm_tests::{ + BLOB_META_KEY, BLOB_META_VALUE, FEE, INT_META_KEY, INT_META_VALUE, NAT_META_KEY, + NAT_META_VALUE, TEXT_META_KEY, TEXT_META_VALUE, TOKEN_NAME, TOKEN_SYMBOL, +}; +use ic_ledger_canister_core::archive::ArchiveOptions; +use ic_registry_subnet_type::SubnetType; +use ic_state_machine_tests::{StateMachine, StateMachineBuilder}; +use icrc_ledger_types::icrc1::account::Account; +use std::time::{Duration, SystemTime}; + +const MINTER_PRINCIPAL: Principal = Principal::from_slice(&[3_u8; 29]); +const STARTING_CYCLES_PER_CANISTER: u128 = 2_000_000_000_000_000; +const ARCHIVE_TRIGGER_THRESHOLD: u64 = 10; +const NUM_BLOCKS_TO_ARCHIVE: usize = 5; +const MAX_BLOCKS_FROM_ARCHIVE: u64 = 10; + +#[test] +fn should_upgrade_and_downgrade_ledger_canister_suite() { + let now = SystemTime::now(); + let env = &StateMachineBuilder::new() + .with_subnet_type(SubnetType::Application) + .with_subnet_size(28) + .build(); + env.set_time(now); + + let ledger_id = install_ledger( + env, + vec![], + default_archive_options(), + None, + MINTER_PRINCIPAL, + ); + let index_id = install_index_ng( + env, + IndexInitArg { + ledger_id: Principal::from(ledger_id), + retrieve_blocks_from_ledger_interval_seconds: None, + }, + ); + + env.advance_time(Duration::from_secs(60)); + env.tick(); + + let index_upgrade_arg = IndexArg::Upgrade(IndexUpgradeArg { + ledger_id: None, + retrieve_blocks_from_ledger_interval_seconds: None, + }); + env.upgrade_canister( + index_id, + index_ng_wasm(), + Encode!(&index_upgrade_arg).unwrap(), + ) + .unwrap(); + + let ledger_upgrade_arg = LedgerArgument::Upgrade(None); + env.upgrade_canister( + ledger_id, + ledger_wasm(), + Encode!(&ledger_upgrade_arg).unwrap(), + ) + .unwrap(); + + env.advance_time(Duration::from_secs(60)); + env.tick(); + + env.upgrade_canister( + index_id, + index_ng_mainnet_wasm(), + Encode!(&index_upgrade_arg).unwrap(), + ) + .unwrap(); + + env.upgrade_canister( + ledger_id, + ledger_mainnet_wasm(), + Encode!(&ledger_upgrade_arg).unwrap(), + ) + .unwrap(); +} + +fn default_archive_options() -> ArchiveOptions { + ArchiveOptions { + trigger_threshold: ARCHIVE_TRIGGER_THRESHOLD as usize, + num_blocks_to_archive: NUM_BLOCKS_TO_ARCHIVE, + node_max_memory_size_bytes: None, + max_message_size_bytes: None, + controller_id: PrincipalId::new_user_test_id(100), + more_controller_ids: None, + cycles_for_archive_creation: None, + max_transactions_per_response: Some(MAX_BLOCKS_FROM_ARCHIVE), + } +} + +fn index_ng_mainnet_wasm() -> Vec { + load_wasm_using_env_var("IC_ICRC1_INDEX_NG_DEPLOYED_VERSION_WASM_PATH") +} + +fn index_ng_wasm() -> Vec { + load_wasm_using_env_var("IC_ICRC1_INDEX_NG_WASM_PATH") +} + +fn install_ledger( + env: &StateMachine, + initial_balances: Vec<(Account, u64)>, + archive_options: ArchiveOptions, + fee_collector_account: Option, + minter_principal: Principal, +) -> CanisterId { + let mut builder = InitArgsBuilder::with_symbol_and_name(TOKEN_SYMBOL, TOKEN_NAME) + .with_minting_account(minter_principal) + .with_transfer_fee(FEE) + .with_metadata_entry(NAT_META_KEY, NAT_META_VALUE) + .with_metadata_entry(INT_META_KEY, INT_META_VALUE) + .with_metadata_entry(TEXT_META_KEY, TEXT_META_VALUE) + .with_metadata_entry(BLOB_META_KEY, BLOB_META_VALUE) + .with_archive_options(archive_options) + .with_feature_flags(FeatureFlags { icrc2: true }); + if let Some(fee_collector_account) = fee_collector_account { + builder = builder.with_fee_collector_account(fee_collector_account); + } + for (account, amount) in initial_balances { + builder = builder.with_initial_balance(account, amount); + } + env.install_canister_with_cycles( + ledger_mainnet_wasm(), + Encode!(&LedgerArgument::Init(builder.build())).unwrap(), + None, + ic_state_machine_tests::Cycles::new(STARTING_CYCLES_PER_CANISTER), + ) + .unwrap() +} + +fn install_index_ng(env: &StateMachine, init_arg: IndexInitArg) -> CanisterId { + let args = IndexArg::Init(init_arg); + env.install_canister_with_cycles( + index_ng_mainnet_wasm(), + Encode!(&args).unwrap(), + None, + ic_state_machine_tests::Cycles::new(STARTING_CYCLES_PER_CANISTER), + ) + .unwrap() +} + +fn ledger_mainnet_wasm() -> Vec { + load_wasm_using_env_var("IC_ICRC1_LEDGER_DEPLOYED_VERSION_WASM_PATH") +} + +fn ledger_wasm() -> Vec { + load_wasm_using_env_var("IC_ICRC1_LEDGER_WASM_PATH") +} + +fn load_wasm_using_env_var(env_var: &str) -> Vec { + let wasm_path = std::env::var(env_var).unwrap_or_else(|e| { + panic!( + "The wasm path must be set using the env variable {} ({})", + env_var, e + ) + }); + std::fs::read(&wasm_path).unwrap_or_else(|e| { + panic!( + "failed to load Wasm file from path {} (env var {}): {}", + wasm_path, env_var, e + ) + }) +} diff --git a/rs/rosetta-api/ledger_canister_blocks_synchronizer/src/blocks.rs b/rs/rosetta-api/ledger_canister_blocks_synchronizer/src/blocks.rs index 4ac453b7de7..88a6d9ddd3c 100644 --- a/rs/rosetta-api/ledger_canister_blocks_synchronizer/src/blocks.rs +++ b/rs/rosetta-api/ledger_canister_blocks_synchronizer/src/blocks.rs @@ -1582,7 +1582,8 @@ impl Blocks { ) -> Result { let (parent_hash, timestamp) = self.get_rosetta_block_phash_timestamp(rosetta_block_index)?; - let transactions = self.get_rosetta_block_transactions(rosetta_block_index)?; + let transactions: BTreeMap = + self.get_rosetta_block_transactions(rosetta_block_index)?; Ok(RosettaBlock { index: rosetta_block_index, parent_hash, @@ -1591,6 +1592,25 @@ impl Blocks { }) } + pub fn get_highest_rosetta_block_index(&self) -> Result, BlockStoreError> { + let connection = self + .connection + .lock() + .map_err(|e| format!("Unable to aquire the connection mutex: {e:?}"))?; + let block_idx = match connection + .prepare_cached( + "SELECT rosetta_block_idx FROM rosetta_blocks ORDER BY rosetta_block_idx DESC LIMIT 1", + ) + .map_err(|e| format!("Unable to prepare query: {e:?}"))?.query_map(params![], |row| + row.get(0) + ).map_err(|e| BlockStoreError::Other(format!("Unable to select from rosetta_blocks: {e:?}")))?.next(){ + Some(Ok(block_idx)) => Some(block_idx), + Some(Err(e)) => return Err(BlockStoreError::Other(e.to_string())), + None => None, + }; + Ok(block_idx) + } + fn get_rosetta_block_phash_timestamp( &self, rosetta_block_index: BlockIndex, diff --git a/rs/rosetta-api/rosetta_core/src/response_types.rs b/rs/rosetta-api/rosetta_core/src/response_types.rs index 5b83e6b462a..34c3b55f19f 100644 --- a/rs/rosetta-api/rosetta_core/src/response_types.rs +++ b/rs/rosetta-api/rosetta_core/src/response_types.rs @@ -75,7 +75,7 @@ impl NetworkStatusResponse { current_block_timestamp: u64, genesis_block_identifier: BlockIdentifier, oldest_block_identifier: Option, - sync_status: SyncStatus, + sync_status: Option, peers: Vec, ) -> NetworkStatusResponse { NetworkStatusResponse { @@ -83,7 +83,7 @@ impl NetworkStatusResponse { current_block_timestamp, genesis_block_identifier, oldest_block_identifier, - sync_status: Some(sync_status), + sync_status, peers, } } diff --git a/rs/rosetta-api/src/request_handler.rs b/rs/rosetta-api/src/request_handler.rs index 40a71fb3ddb..eba876fda13 100644 --- a/rs/rosetta-api/src/request_handler.rs +++ b/rs/rosetta-api/src/request_handler.rs @@ -7,13 +7,23 @@ mod construction_payloads; mod construction_preprocess; mod construction_submit; +use crate::convert::{from_model_account_identifier, neuron_account_from_public_key}; +use crate::errors::{ApiError, Details}; use crate::ledger_client::list_known_neurons_response::ListKnownNeuronsResponse; use crate::ledger_client::pending_proposals_response::PendingProposalsResponse; use crate::ledger_client::proposal_info_response::ProposalInfoResponse; +use crate::ledger_client::LedgerAccess; +use crate::models::amount::tokens_to_amount; use crate::models::{ AccountBalanceMetadata, CallResponse, NetworkIdentifier, QueryBlockRangeRequest, QueryBlockRangeResponse, }; +use crate::models::{ + AccountBalanceRequest, AccountBalanceResponse, Allow, BalanceAccountType, BlockIdentifier, + BlockResponse, BlockTransaction, BlockTransactionResponse, Error, NetworkOptionsResponse, + NetworkStatusResponse, NeuronInfoResponse, NeuronState, NeuronSubaccountComponents, + OperationStatus, Operator, PartialBlockIdentifier, SearchTransactionsResponse, Version, +}; use crate::request_types::GetProposalInfo; use crate::transaction_id::TransactionIdentifier; use crate::{convert, models, API_VERSION, MAX_BLOCKS_PER_QUERY_BLOCK_RANGE_REQUEST, NODE_VERSION}; @@ -22,25 +32,12 @@ use ic_ledger_canister_blocks_synchronizer::blocks::RosettaBlocksMode; use ic_ledger_canister_blocks_synchronizer::rosetta_block::RosettaBlock; use ic_ledger_core::block::BlockType; use ic_nns_common::pb::v1::NeuronId; -use rosetta_core::objects::ObjectMap; -use tracing::info; - -use crate::convert::{from_model_account_identifier, neuron_account_from_public_key}; -use crate::errors::{ApiError, Details}; -use crate::ledger_client::LedgerAccess; -use crate::models::amount::tokens_to_amount; -use crate::models::{ - AccountBalanceRequest, AccountBalanceResponse, Allow, BalanceAccountType, BlockIdentifier, - BlockResponse, BlockTransaction, BlockTransactionResponse, Error, NetworkOptionsResponse, - NetworkStatusResponse, NeuronInfoResponse, NeuronState, NeuronSubaccountComponents, - OperationStatus, Operator, PartialBlockIdentifier, SearchTransactionsResponse, SyncStatus, - Version, -}; use ic_nns_governance::pb::v1::manage_neuron::NeuronIdOrSubaccount; use ic_types::crypto::DOMAIN_IC_REQUEST; use ic_types::messages::MessageId; use ic_types::CanisterId; use icp_ledger::{Block, BlockIndex}; +use rosetta_core::objects::ObjectMap; use rosetta_core::request_types::MetadataRequest; use rosetta_core::response_types::NetworkListResponse; use rosetta_core::response_types::{MempoolResponse, MempoolTransactionResponse}; @@ -220,7 +217,6 @@ impl RosettaRequestHandler { if storage.contains_block(&lowest_index).map_err(|err| { ApiError::InvalidBlockId(false, format!("{:?}", err).into()) })? { - info!("Querying verified blocks"); // TODO: Use block range with rosetta blocks for hb in storage .get_hashed_block_range( @@ -331,7 +327,12 @@ impl RosettaRequestHandler { } _ => { if self.is_rosetta_blocks_mode_enabled().await { - todo!("Fetching the latest block is not supported yet") + let blocks = self.ledger.read_blocks().await; + let highest_block_index = blocks + .get_highest_rosetta_block_index() + .map_err(ApiError::from)? + .ok_or_else(|| ApiError::BlockchainEmpty(false, Default::default()))?; + self.get_rosetta_block_by_index(highest_block_index).await } else { self.get_latest_verified_block().await } @@ -365,6 +366,7 @@ impl RosettaRequestHandler { block: HashedBlock, ) -> Result { let parent_block_id = self.create_parent_block_id(block.index).await?; + let token_symbol = self.ledger.token_symbol(); hashed_block_to_rosetta_core_block(block, parent_block_id, token_symbol) } @@ -561,41 +563,74 @@ impl RosettaRequestHandler { ) -> Result { verify_network_id(self.ledger.ledger_canister_id(), &msg.network_identifier)?; let blocks = self.ledger.read_blocks().await; - let first = blocks.get_first_verified_hashed_block()?; - let tip = blocks.get_latest_verified_hashed_block()?; - let tip_id = convert::block_id(&tip)?; - let tip_timestamp = models::timestamp::from_system_time( - Block::decode(tip.block).unwrap().timestamp.into(), - )?; - - let genesis_block = blocks.get_hashed_block(&0)?; - let genesis_block_id = convert::block_id(&genesis_block)?; - let peers = vec![]; - let oldest_block_id = if first.index != 0 { - Some(convert::block_id(&first)?) - } else { - None + let network_status = match blocks.rosetta_blocks_mode { + // If rosetta mode is not enabled we simply fetched the latest verified block + RosettaBlocksMode::Disabled => { + let tip_verified_block = blocks.get_latest_verified_hashed_block()?; + let genesis_block = blocks.get_hashed_block(&0)?; + let first_verified_block = blocks.get_first_verified_hashed_block()?; + let oldest_block_id = if first_verified_block.index != 0 { + Some(convert::block_id(&first_verified_block)?) + } else { + None + }; + NetworkStatusResponse::new( + convert::block_id(&tip_verified_block)?, + models::timestamp::from_system_time( + Block::decode(tip_verified_block.block) + .unwrap() + .timestamp + .into(), + )? + .0 + .try_into() + .map_err(|err: TryFromIntError| { + ApiError::InternalError( + false, + Details::from(format!("Cannot convert timestamp to u64: {}", err)), + ) + })?, + convert::block_id(&genesis_block)?, + oldest_block_id, + None, + vec![], + ) + } + RosettaBlocksMode::Enabled { + first_rosetta_block_index, + } => { + // If rosetta blocks mode is enabled we have to check whether the rosetta blocks table has been populated + match blocks.get_highest_rosetta_block_index()? { + // If it has been populated we can return the highest rosetta block + Some(highest_rosetta_block_index) => { + let highest_rosetta_block = self + .get_rosetta_block_by_index(highest_rosetta_block_index) + .await?; + // If Rosetta Blocks started only after a certain index then the genesis block as well as the first verified block will be the first icp block + let genesis_block_id = if first_rosetta_block_index > 0 { + self.hashed_block_to_rosetta_core_block(blocks.get_hashed_block(&0)?) + .await? + .block_identifier + } else { + self.get_rosetta_block_by_index(0).await?.block_identifier + }; + NetworkStatusResponse::new( + highest_rosetta_block.block_identifier, + highest_rosetta_block.timestamp, + genesis_block_id, + None, + None, + vec![], + ) + } + None => { + return Err(ApiError::BlockchainEmpty(false, "RosettaBlocks was activated and there are no RosettaBlocks in the database yet. The synch is ongoing, please wait until the first RosettaBlock is written to the database.".into())); + } + } + } }; - let mut sync_status = SyncStatus::new(tip.index as i64, None); - let target = crate::rosetta_server::TARGET_HEIGHT.get(); - if target != 0 { - sync_status.target_index = Some(crate::rosetta_server::TARGET_HEIGHT.get()); - } - - Ok(NetworkStatusResponse::new( - tip_id, - tip_timestamp.0.try_into().map_err(|err: TryFromIntError| { - ApiError::InternalError( - false, - Details::from(format!("Cannot convert timestamp to u64: {}", err)), - ) - })?, - genesis_block_id, - oldest_block_id, - sync_status, - peers, - )) + Ok(network_status) } async fn get_blocks_range( diff --git a/rs/rosetta-api/tests/basic_tests.rs b/rs/rosetta-api/tests/basic_tests.rs index e2b1c165c15..f7116442199 100644 --- a/rs/rosetta-api/tests/basic_tests.rs +++ b/rs/rosetta-api/tests/basic_tests.rs @@ -19,7 +19,7 @@ use ic_rosetta_api::models::{ BlockTransactionRequest, ConstructionDeriveRequest, ConstructionDeriveResponse, ConstructionMetadataRequest, ConstructionMetadataResponse, Currency, CurveType, MempoolTransactionRequest, NetworkRequest, NetworkStatusResponse, SearchTransactionsRequest, - SearchTransactionsResponse, SyncStatus, + SearchTransactionsResponse, }; use ic_rosetta_api::request_handler::RosettaRequestHandler; use ic_rosetta_api::transaction_id::TransactionIdentifier; @@ -93,12 +93,7 @@ async fn smoke_test() { .unwrap(), block_id(scribe.blockchain.front().unwrap()).unwrap(), None, - SyncStatus { - current_index: scribe.blockchain.back().unwrap().index as i64, - target_index: None, - stage: None, - synced: None - }, + None, vec![] )) ); diff --git a/rs/tests/Cargo.toml b/rs/tests/Cargo.toml index 3683ffc0f39..db0139c5e8b 100644 --- a/rs/tests/Cargo.toml +++ b/rs/tests/Cargo.toml @@ -220,8 +220,8 @@ name = "ic-systest-rosetta-network-test" path = "financial_integrations/rosetta/rosetta_network_test.rs" [[bin]] -name = "ic-systest-rosetta-neuron-disbourse-test" -path = "financial_integrations/rosetta/rosetta_neuron_disbourse_test.rs" +name = "ic-systest-rosetta-neuron-disburse-test" +path = "financial_integrations/rosetta/rosetta_neuron_disburse_test.rs" [[bin]] name = "ic-systest-rosetta-neuron-dissolve-test" diff --git a/rs/tests/financial_integrations/rosetta/BUILD.bazel b/rs/tests/financial_integrations/rosetta/BUILD.bazel index ba0459b3a9e..3983e263507 100644 --- a/rs/tests/financial_integrations/rosetta/BUILD.bazel +++ b/rs/tests/financial_integrations/rosetta/BUILD.bazel @@ -77,7 +77,7 @@ system_test_nns( ) system_test_nns( - name = "rosetta_neuron_disbourse_test", + name = "rosetta_neuron_disburse_test", flaky = True, proc_macro_deps = MACRO_DEPENDENCIES, tags = [ diff --git a/rs/tests/financial_integrations/rosetta/rosetta_neuron_disbourse_test.rs b/rs/tests/financial_integrations/rosetta/rosetta_neuron_disburse_test.rs similarity index 100% rename from rs/tests/financial_integrations/rosetta/rosetta_neuron_disbourse_test.rs rename to rs/tests/financial_integrations/rosetta/rosetta_neuron_disburse_test.rs