Skip to content

Commit

Permalink
Merge branch 'maciej-maxblocks' into 'master'
Browse files Browse the repository at this point in the history
feat(ICP-Ledger): limit endpoints returning blocks to 50 blocks/request for ingress replicated queries

 

See merge request dfinity-lab/public/ic!19311
  • Loading branch information
maciejdfinity committed May 29, 2024
2 parents debb389 + ccc04ec commit 57c3293
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 11 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rs/rosetta-api/icp_ledger/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions rs/rosetta-api/icp_ledger/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
22 changes: 13 additions & 9 deletions rs/rosetta-api/icp_ledger/ledger/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
});
}
Expand All @@ -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)
Expand All @@ -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<CandidBlock> = ledger
.blockchain
Expand Down Expand Up @@ -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();

Expand Down
213 changes: 211 additions & 2 deletions rs/rosetta-api/icp_ledger/ledger/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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 =
Expand Down
8 changes: 8 additions & 0 deletions rs/rosetta-api/icp_ledger/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 57c3293

Please sign in to comment.