From ccc04ecbd5643315f2eded88c82587a2626a8b52 Mon Sep 17 00:00:00 2001 From: Maciej Modelski Date: Wed, 29 May 2024 16:27:42 +0000 Subject: [PATCH] feat(ICP-Ledger): limit endpoints returning blocks to 50 blocks/request for ingress replicated queries --- Cargo.lock | 1 + rs/rosetta-api/icp_ledger/BUILD.bazel | 1 + rs/rosetta-api/icp_ledger/Cargo.toml | 1 + rs/rosetta-api/icp_ledger/ledger/src/main.rs | 22 +- .../icp_ledger/ledger/tests/tests.rs | 213 +++++++++++++++++- rs/rosetta-api/icp_ledger/src/lib.rs | 8 + 6 files changed, 235 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19724a2ae6e..c636efdb717 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12491,6 +12491,7 @@ dependencies = [ "ic-base-types", "ic-canister-client-sender", "ic-canisters-http-types", + "ic-cdk 0.13.2", "ic-crypto-sha2", "ic-icp-index", "ic-ledger-canister-core", diff --git a/rs/rosetta-api/icp_ledger/BUILD.bazel b/rs/rosetta-api/icp_ledger/BUILD.bazel index ff3fc8851ee..452100d8229 100644 --- a/rs/rosetta-api/icp_ledger/BUILD.bazel +++ b/rs/rosetta-api/icp_ledger/BUILD.bazel @@ -43,6 +43,7 @@ rust_library( "@crate_index//:comparable", "@crate_index//:crc32fast", "@crate_index//:hex", + "@crate_index//:ic-cdk", "@crate_index//:num-traits", "@crate_index//:prost", "@crate_index//:serde", diff --git a/rs/rosetta-api/icp_ledger/Cargo.toml b/rs/rosetta-api/icp_ledger/Cargo.toml index e57754b0097..6689ab029f9 100644 --- a/rs/rosetta-api/icp_ledger/Cargo.toml +++ b/rs/rosetta-api/icp_ledger/Cargo.toml @@ -16,6 +16,7 @@ dfn_protobuf = { path = "../../rust_canisters/dfn_protobuf" } hex = { workspace = true } ic-base-types = { path = "../../types/base_types" } ic-crypto-sha2 = { path = "../../crypto/sha2" } +ic-cdk = { workspace = true } ic-ledger-canister-core = { path = "../ledger_canister_core" } ic-ledger-core = { path = "../ledger_core" } ic-ledger-hash-of = { path = "../../../packages/ic-ledger-hash-of" } diff --git a/rs/rosetta-api/icp_ledger/ledger/src/main.rs b/rs/rosetta-api/icp_ledger/ledger/src/main.rs index 62e0421b9e9..a6293f87f81 100644 --- a/rs/rosetta-api/icp_ledger/ledger/src/main.rs +++ b/rs/rosetta-api/icp_ledger/ledger/src/main.rs @@ -26,13 +26,13 @@ use ic_ledger_core::{ tokens::{Tokens, DECIMAL_PLACES}, }; use icp_ledger::{ - protobuf, tokens_into_proto, AccountBalanceArgs, AccountIdBlob, AccountIdentifier, ArchiveInfo, - ArchivedBlocksRange, ArchivedEncodedBlocksRange, Archives, BinaryAccountBalanceArgs, Block, - BlockArg, BlockRes, CandidBlock, Decimals, FeatureFlags, GetBlocksArgs, InitArgs, - IterBlocksArgs, LedgerCanisterPayload, Memo, Name, Operation, PaymentError, - QueryBlocksResponse, QueryEncodedBlocksResponse, SendArgs, Subaccount, Symbol, TipOfChainRes, - TotalSupplyArgs, Transaction, TransferArgs, TransferError, TransferFee, TransferFeeArgs, - MAX_BLOCKS_PER_REQUEST, MEMO_SIZE_BYTES, + max_blocks_per_request, protobuf, tokens_into_proto, AccountBalanceArgs, AccountIdBlob, + AccountIdentifier, ArchiveInfo, ArchivedBlocksRange, ArchivedEncodedBlocksRange, Archives, + BinaryAccountBalanceArgs, Block, BlockArg, BlockRes, CandidBlock, Decimals, FeatureFlags, + GetBlocksArgs, InitArgs, IterBlocksArgs, LedgerCanisterPayload, Memo, Name, Operation, + PaymentError, QueryBlocksResponse, QueryEncodedBlocksResponse, SendArgs, Subaccount, Symbol, + TipOfChainRes, TotalSupplyArgs, Transaction, TransferArgs, TransferError, TransferFee, + TransferFeeArgs, MEMO_SIZE_BYTES, }; use icrc_ledger_types::{ icrc::generic_metadata_value::MetadataValue as Value, @@ -1154,6 +1154,7 @@ fn icrc1_total_supply_candid() { fn iter_blocks_() { over(protobuf, |IterBlocksArgs { start, length }| { let blocks = &LEDGER.read().unwrap().blockchain.blocks; + let length = std::cmp::min(length, max_blocks_per_request(&caller())); icp_ledger::iter_blocks(blocks, start, length) }); } @@ -1163,6 +1164,7 @@ fn iter_blocks_() { #[export_name = "canister_query get_blocks_pb"] fn get_blocks_() { over(protobuf, |GetBlocksArgs { start, length }| { + let length = std::cmp::min(length, max_blocks_per_request(&caller())); let blockchain = &LEDGER.read().unwrap().blockchain; let start_offset = blockchain.num_archived_blocks(); icp_ledger::get_blocks(&blockchain.blocks, start_offset, start, length) @@ -1179,7 +1181,8 @@ fn query_blocks(GetBlocksArgs { start, length }: GetBlocksArgs) -> QueryBlocksRe let ledger = LEDGER.read().unwrap(); let locations = block_locations(&*ledger, start, length); - let local_blocks = range_utils::take(&locations.local_blocks, MAX_BLOCKS_PER_REQUEST); + let local_blocks = + range_utils::take(&locations.local_blocks, max_blocks_per_request(&caller())); let blocks: Vec = ledger .blockchain @@ -1367,7 +1370,8 @@ fn query_encoded_blocks( let ledger = LEDGER.read().unwrap(); let locations = block_locations(&*ledger, start, length); - let local_blocks = range_utils::take(&locations.local_blocks, MAX_BLOCKS_PER_REQUEST); + let local_blocks = + range_utils::take(&locations.local_blocks, max_blocks_per_request(&caller())); let blocks = ledger.blockchain.block_slice(local_blocks.clone()).to_vec(); diff --git a/rs/rosetta-api/icp_ledger/ledger/tests/tests.rs b/rs/rosetta-api/icp_ledger/ledger/tests/tests.rs index dac626dfeef..37daee69615 100644 --- a/rs/rosetta-api/icp_ledger/ledger/tests/tests.rs +++ b/rs/rosetta-api/icp_ledger/ledger/tests/tests.rs @@ -12,8 +12,9 @@ use ic_state_machine_tests::{ErrorCode, PrincipalId, StateMachine, UserError}; use icp_ledger::{ AccountIdBlob, AccountIdentifier, ArchiveOptions, ArchivedBlocksRange, Block, CandidBlock, CandidOperation, CandidTransaction, FeatureFlags, GetBlocksArgs, GetBlocksRes, GetBlocksResult, - InitArgs, LedgerCanisterInitPayload, LedgerCanisterPayload, Operation, QueryBlocksResponse, - QueryEncodedBlocksResponse, TimeStamp, UpgradeArgs, DEFAULT_TRANSFER_FEE, + InitArgs, IterBlocksArgs, IterBlocksRes, LedgerCanisterInitPayload, LedgerCanisterPayload, + Operation, QueryBlocksResponse, QueryEncodedBlocksResponse, TimeStamp, UpgradeArgs, + DEFAULT_TRANSFER_FEE, MAX_BLOCKS_PER_INGRESS_REPLICATED_QUERY_REQUEST, MAX_BLOCKS_PER_REQUEST, }; use icrc_ledger_types::icrc1::{ account::Account, @@ -131,6 +132,28 @@ fn get_blocks_pb( result } +fn iter_blocks_pb( + env: &StateMachine, + caller: Principal, + ledger: CanisterId, + start: usize, + length: usize, +) -> IterBlocksRes { + let bytes = env + .execute_ingress_as( + PrincipalId(caller), + ledger, + "iter_blocks_pb", + ProtoBuf(IterBlocksArgs { start, length }) + .into_bytes() + .unwrap(), + ) + .expect("failed to query blocks") + .bytes(); + let result: IterBlocksRes = ProtoBuf::from_bytes(bytes).map(|c| c.0).unwrap(); + result +} + fn account_identifier(env: &StateMachine, ledger: CanisterId, account: Account) -> AccountIdBlob { let bytes = env .query(ledger, "account_identifier", Encode!(&account).unwrap()) @@ -413,6 +436,192 @@ fn check_query_blocks_coherence() { ); } +#[test] +fn check_block_endpoint_limits() { + let ledger_wasm_current = ledger_wasm(); + + let user_principal = + Principal::from_text("luwgt-ouvkc-k5rx5-xcqkq-jx5hm-r2rj2-ymqjc-pjvhb-kij4p-n4vms-gqe") + .unwrap(); + let canister_principal = Principal::from_text("2chl6-4hpzw-vqaaa-aaaaa-c").unwrap(); + + let env = StateMachine::new(); + let mut initial_balances = HashMap::new(); + for i in 0..MAX_BLOCKS_PER_REQUEST + 1 { + let p = PrincipalId::new_user_test_id(i as u64 + 1); + initial_balances.insert(Account::from(p.0).into(), Tokens::from_e8s(1)); + } + let payload = LedgerCanisterInitPayload::builder() + .archive_options(ArchiveOptions { + trigger_threshold: 50000, + num_blocks_to_archive: 2, + node_max_memory_size_bytes: None, + max_message_size_bytes: None, + controller_id: PrincipalId::new_anonymous(), + more_controller_ids: None, + cycles_for_archive_creation: None, + max_transactions_per_response: None, + }) + .minting_account(MINTER.into()) + .icrc1_minting_account(MINTER) + .initial_values(initial_balances) + .transfer_fee(Tokens::from_e8s(10_000)) + .token_symbol_and_name("ICP", "Internet Computer") + .build() + .unwrap(); + let canister_id = env + .install_canister( + ledger_wasm_current, + CandidOne(payload).into_bytes().unwrap(), + None, + ) + .expect("Unable to install the Ledger canister with the new init"); + + let get_blocks_args = Encode!(&GetBlocksArgs { + start: 0, + length: MAX_BLOCKS_PER_REQUEST + 1 + }) + .unwrap(); + + // query_blocks + let ingress_update = query_blocks(&env, user_principal, canister_id, 0, u32::MAX.into()); + let canister_update = query_blocks(&env, canister_principal, canister_id, 0, u32::MAX.into()); + let query = Decode!( + &env.query_as( + PrincipalId(user_principal), + canister_id, + "query_blocks".to_string(), + get_blocks_args.clone(), + ) + .expect("query failed") + .bytes(), + QueryBlocksResponse + ) + .expect("failed to decode response"); + + assert_eq!( + ingress_update.blocks.len(), + MAX_BLOCKS_PER_INGRESS_REPLICATED_QUERY_REQUEST + ); + assert_eq!(canister_update.blocks.len(), MAX_BLOCKS_PER_REQUEST); + assert_eq!(query.blocks.len(), MAX_BLOCKS_PER_REQUEST); + + // query_encoded_blocks + let ingress_update = + query_encoded_blocks(&env, user_principal, canister_id, 0, u32::MAX.into()); + let canister_update = + query_encoded_blocks(&env, canister_principal, canister_id, 0, u32::MAX.into()); + let query = Decode!( + &env.query_as( + user_principal.into(), + canister_id, + "query_encoded_blocks".to_string(), + get_blocks_args.clone(), + ) + .expect("query failed") + .bytes(), + QueryEncodedBlocksResponse + ) + .expect("failed to decode response"); + + assert_eq!( + ingress_update.blocks.len(), + MAX_BLOCKS_PER_INGRESS_REPLICATED_QUERY_REQUEST + ); + assert_eq!(canister_update.blocks.len(), MAX_BLOCKS_PER_REQUEST); + assert_eq!(query.blocks.len(), MAX_BLOCKS_PER_REQUEST); + + // get_blocks_pb + let get_blocks_pb_args = ProtoBuf(GetBlocksArgs { + start: 0, + length: MAX_BLOCKS_PER_REQUEST + 1, + }) + .into_bytes() + .unwrap(); + + let ingress_update = get_blocks_pb( + &env, + user_principal, + canister_id, + 0, + MAX_BLOCKS_PER_REQUEST + 1, + ); + let canister_update = get_blocks_pb( + &env, + canister_principal, + canister_id, + 0, + MAX_BLOCKS_PER_REQUEST + 1, + ); + let query: GetBlocksRes = ProtoBuf::from_bytes( + env.query_as( + user_principal.into(), + canister_id, + "get_blocks_pb".to_string(), + get_blocks_pb_args.clone(), + ) + .expect("query failed") + .bytes(), + ) + .map(|c| c.0) + .unwrap(); + + assert_eq!( + ingress_update.0.expect("failed to get blocks").len(), + MAX_BLOCKS_PER_INGRESS_REPLICATED_QUERY_REQUEST + ); + assert_eq!( + canister_update.0.expect("failed to get blocks").len(), + MAX_BLOCKS_PER_REQUEST + ); + assert_eq!( + query.0.expect("failed to get blocks").len(), + MAX_BLOCKS_PER_REQUEST + ); + + // iter_blocks_pb + let iter_blocks_pb_args = ProtoBuf(IterBlocksArgs { + start: 0, + length: MAX_BLOCKS_PER_REQUEST + 1, + }) + .into_bytes() + .unwrap(); + + let ingress_update = iter_blocks_pb( + &env, + user_principal, + canister_id, + 0, + MAX_BLOCKS_PER_REQUEST + 1, + ); + let canister_update = iter_blocks_pb( + &env, + canister_principal, + canister_id, + 0, + MAX_BLOCKS_PER_REQUEST + 1, + ); + let query: IterBlocksRes = ProtoBuf::from_bytes( + env.query_as( + user_principal.into(), + canister_id, + "iter_blocks_pb".to_string(), + iter_blocks_pb_args.clone(), + ) + .expect("query failed") + .bytes(), + ) + .map(|c| c.0) + .unwrap(); + + assert_eq!( + ingress_update.0.len(), + MAX_BLOCKS_PER_INGRESS_REPLICATED_QUERY_REQUEST + ); + assert_eq!(canister_update.0.len(), MAX_BLOCKS_PER_REQUEST); + assert_eq!(query.0.len(), MAX_BLOCKS_PER_REQUEST); +} + #[test] fn test_block_transformation() { let ledger_wasm_mainnet = diff --git a/rs/rosetta-api/icp_ledger/src/lib.rs b/rs/rosetta-api/icp_ledger/src/lib.rs index 7b96c78eb1b..805941887f3 100644 --- a/rs/rosetta-api/icp_ledger/src/lib.rs +++ b/rs/rosetta-api/icp_ledger/src/lib.rs @@ -43,6 +43,7 @@ pub use validate_endpoints::{tokens_from_proto, tokens_into_proto}; pub const DEFAULT_TRANSFER_FEE: Tokens = Tokens::from_e8s(10_000); pub const MAX_BLOCKS_PER_REQUEST: usize = 2000; +pub const MAX_BLOCKS_PER_INGRESS_REPLICATED_QUERY_REQUEST: usize = 50; pub const MEMO_SIZE_BYTES: usize = 32; @@ -1252,6 +1253,13 @@ impl Default for FeatureFlags { } } +pub fn max_blocks_per_request(principal_id: &PrincipalId) -> usize { + if ic_cdk::api::data_certificate().is_none() && principal_id.is_self_authenticating() { + return MAX_BLOCKS_PER_INGRESS_REPLICATED_QUERY_REQUEST; + } + MAX_BLOCKS_PER_REQUEST +} + #[cfg(test)] mod test { use std::str::FromStr;