Skip to content

Commit

Permalink
Merge branch 'DD-536-fetch-block-interval' into 'master'
Browse files Browse the repository at this point in the history
feat(ICRC-Rosetta): DD-536: Fetch block interval

This MR introduces the following changes:
1. Move Call request and response to rosetta core
2. Add a call endpoint to icrc rosetta
3. Add a query_block_range option for the call endpoint 

See merge request dfinity-lab/public/ic!20147
  • Loading branch information
NikolasHai committed Jul 5, 2024
2 parents f7534e8 + 2b91ee5 commit 52b5c41
Show file tree
Hide file tree
Showing 14 changed files with 466 additions and 53 deletions.
5 changes: 3 additions & 2 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/icrc1/rosetta/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Added /ready endpoint which indicates whether Rosetta is finished with its initial block synch
- /call endpoint with the method 'query_block_range' to fetch multiple blocks at once
### Fixes
- Changed default database path to match /data/db.sqlite

Expand Down
18 changes: 18 additions & 0 deletions rs/rosetta-api/icrc1/rosetta/client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use num_bigint::BigInt;
use reqwest::{Client, Url};
use rosetta_core::identifiers::*;
use rosetta_core::models::RosettaSupportedKeyPair;
use rosetta_core::objects::ObjectMap;
use rosetta_core::objects::Operation;
use rosetta_core::objects::PublicKey;
use rosetta_core::objects::Signature;
Expand Down Expand Up @@ -693,4 +694,21 @@ impl RosettaClient {
)
.await
}

pub async fn call(
&self,
network_identifier: NetworkIdentifier,
method_name: String,
parameters: ObjectMap,
) -> Result<CallResponse, Error> {
self.call_endpoint(
"/call",
&CallRequest {
network_identifier,
method_name,
parameters,
},
)
.await
}
}
1 change: 1 addition & 0 deletions rs/rosetta-api/icrc1/rosetta/src/common/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ pub const FEE_COLLECTOR_OPERATION_IDENTIFIER: u64 = 8;
pub const MAX_TRANSACTIONS_PER_SEARCH_TRANSACTIONS_REQUEST: u64 = 10000;
pub const INGRESS_INTERVAL_OVERLAP: Duration = Duration::from_secs(120);
pub const STATUS_COMPLETED: &str = "COMPLETED";
pub const MAX_BLOCKS_PER_QUERY_BLOCK_RANGE_REQUEST: u64 = 10000;
17 changes: 17 additions & 0 deletions rs/rosetta-api/icrc1/rosetta/src/data_api/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,20 @@ pub async fn search_transactions(
state.metadata.decimals,
)?))
}

pub async fn call(
State(state): State<Arc<AppState>>,
Json(request): Json<CallRequest>,
) -> Result<Json<CallResponse>> {
verify_network_id(&request.network_identifier, &state)
.map_err(|err| Error::invalid_network_id(&format!("{:?}", err)))?;
Ok(Json(services::call(
&state.storage,
&request.method_name,
request.parameters,
rosetta_core::objects::Currency::new(
state.metadata.symbol.clone(),
state.metadata.decimals.into(),
),
)?))
}
1 change: 1 addition & 0 deletions rs/rosetta-api/icrc1/rosetta/src/data_api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod endpoints;
pub mod services;
pub mod types;
232 changes: 232 additions & 0 deletions rs/rosetta-api/icrc1/rosetta/src/data_api/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::sync::Arc;
use std::sync::Mutex;

use crate::common::constants::DEFAULT_BLOCKCHAIN;
use crate::common::constants::MAX_BLOCKS_PER_QUERY_BLOCK_RANGE_REQUEST;
use crate::common::constants::MAX_TRANSACTIONS_PER_SEARCH_TRANSACTIONS_REQUEST;
use crate::common::constants::STATUS_COMPLETED;
use crate::common::types::OperationType;
Expand All @@ -15,6 +16,8 @@ use crate::common::{
icrc1_rosetta_block_to_rosetta_core_transaction,
},
};
use crate::data_api::types::QueryBlockRangeRequest;
use crate::data_api::types::QueryBlockRangeResponse;
use candid::Nat;
use candid::Principal;
use ic_ledger_core::tokens::Zero;
Expand Down Expand Up @@ -460,6 +463,60 @@ pub fn initial_sync_is_completed(
}
}

pub fn call(
storage_client: &StorageClient,
method_name: &str,
parameters: ObjectMap,
currency: Currency,
) -> Result<CallResponse, Error> {
match method_name {
"query_block_range" => {
let query_block_range = QueryBlockRangeRequest::try_from(parameters)
.map_err(|err| Error::parsing_unsuccessful(&err))?;
let mut blocks = vec![];
if query_block_range.number_of_blocks > 0 {
let highest_index = query_block_range.highest_block_index;
let lowest_index = query_block_range.highest_block_index.saturating_sub(
std::cmp::min(
query_block_range.number_of_blocks,
MAX_BLOCKS_PER_QUERY_BLOCK_RANGE_REQUEST,
)
.saturating_sub(1),
);
blocks.extend(
storage_client
.get_blocks_by_index_range(lowest_index, highest_index)
.map_err(|err| Error::unable_to_find_block(&err))?
.into_iter()
.map(|block| {
icrc1_rosetta_block_to_rosetta_core_block(block, currency.clone())
})
.collect::<anyhow::Result<Vec<Block>>>()
.map_err(|err| Error::parsing_unsuccessful(&err))?,
)
};
let idempotent = match blocks.last() {
// If the last block in the database has the same index as the highest block index in the query we return true
Some(last_block) => {
last_block.block_identifier.index == query_block_range.highest_block_index
}
// If the database is empty or the requested block range does not exist we return false
None => false,
};
let block_range_response = QueryBlockRangeResponse { blocks };
Ok(CallResponse::new(
ObjectMap::try_from(block_range_response)
.map_err(|err| Error::parsing_unsuccessful(&err))?,
idempotent,
))
}
_ => Err(Error::processing_construction_failed(&format!(
"Method {} not supported",
method_name
))),
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down Expand Up @@ -1186,4 +1243,179 @@ mod test {
);
assert!(block_res.is_err());
}

#[test]
fn test_call_query_blocks() {
let mut runner = TestRunner::new(TestRunnerConfig {
max_shrink_iters: 0,
cases: 1,
..Default::default()
});

runner
.run(
&(valid_blockchain_strategy::<U256>(BLOCKCHAIN_LENGTH * 25).no_shrink()),
|blockchain| {
let storage_client_memory = StorageClient::new_in_memory().unwrap();
let mut rosetta_blocks = vec![];

let currency = Currency::new("ICP".to_string(), 8);

// Call on an empty database
let response: QueryBlockRangeResponse = call(
&storage_client_memory,
"query_block_range",
ObjectMap::try_from(QueryBlockRangeRequest {
highest_block_index: 100,
number_of_blocks: 10,
})
.unwrap(),
currency.clone(),
)
.unwrap()
.result
.try_into()
.unwrap();
assert!(response.blocks.is_empty());

for (index, block) in blockchain.clone().into_iter().enumerate() {
rosetta_blocks.push(
RosettaBlock::from_generic_block(
encoded_block_to_generic_block(&block.encode()),
index as u64,
)
.unwrap(),
);
}

storage_client_memory
.store_blocks(rosetta_blocks.clone())
.unwrap();
let highest_block_index = rosetta_blocks.len().saturating_sub(1) as u64;
// Call with 0 numbers of blocks
let response: QueryBlockRangeResponse = call(
&storage_client_memory,
"query_block_range",
ObjectMap::try_from(QueryBlockRangeRequest {
highest_block_index,
number_of_blocks: 0,
})
.unwrap(),
currency.clone(),
)
.unwrap()
.result
.try_into()
.unwrap();
assert!(response.blocks.is_empty());

// Call with higher index than there are blocks in the database
let response = call(
&storage_client_memory,
"query_block_range",
ObjectMap::try_from(QueryBlockRangeRequest {
highest_block_index: (rosetta_blocks.len() * 2) as u64,
number_of_blocks: std::cmp::max(
rosetta_blocks.len() as u64,
MAX_BLOCKS_PER_QUERY_BLOCK_RANGE_REQUEST,
),
})
.unwrap(),
currency.clone(),
)
.unwrap();
let query_block_response: QueryBlockRangeResponse =
response.result.try_into().unwrap();
// If the blocks measured from the highest block index asked for are not in the database the service should return an empty array of blocks
if rosetta_blocks.len() >= MAX_BLOCKS_PER_QUERY_BLOCK_RANGE_REQUEST as usize {
assert_eq!(query_block_response.blocks.len(), 0);
assert!(!response.idempotent);
}
// If some of the blocks measured from the highest block index asked for are in the database the service should return the blocks that are in the database
else {
if rosetta_blocks.len() * 2
> MAX_BLOCKS_PER_QUERY_BLOCK_RANGE_REQUEST as usize
{
assert_eq!(
query_block_response.blocks.len(),
rosetta_blocks
.len()
.saturating_sub((rosetta_blocks.len() * 2).saturating_sub(
MAX_BLOCKS_PER_QUERY_BLOCK_RANGE_REQUEST as usize
))
.saturating_sub(1)
);
} else {
assert_eq!(query_block_response.blocks.len(), rosetta_blocks.len());
}
assert!(!response.idempotent);
}

let number_of_blocks = (rosetta_blocks.len() / 2) as u64;
let query_blocks_request = QueryBlockRangeRequest {
highest_block_index,
number_of_blocks,
};

let query_blocks_response = call(
&storage_client_memory,
"query_block_range",
ObjectMap::try_from(query_blocks_request).unwrap(),
currency.clone(),
)
.unwrap();

assert!(query_blocks_response.idempotent);
let response: QueryBlockRangeResponse =
query_blocks_response.result.try_into().unwrap();
let querried_blocks = response.blocks;
assert_eq!(
querried_blocks.len(),
std::cmp::min(number_of_blocks, MAX_BLOCKS_PER_QUERY_BLOCK_RANGE_REQUEST)
as usize
);
if !querried_blocks.is_empty() {
assert_eq!(
querried_blocks.first().unwrap().block_identifier.index,
highest_block_index
.saturating_sub(std::cmp::min(
number_of_blocks,
MAX_BLOCKS_PER_QUERY_BLOCK_RANGE_REQUEST
))
.saturating_add(1)
);
assert_eq!(
querried_blocks.last().unwrap().block_identifier.index,
highest_block_index
);
}

let query_blocks_request = QueryBlockRangeRequest {
highest_block_index,
number_of_blocks: MAX_BLOCKS_PER_QUERY_BLOCK_RANGE_REQUEST + 1,
};

let query_blocks_response: QueryBlockRangeResponse = call(
&storage_client_memory,
"query_block_range",
ObjectMap::try_from(query_blocks_request).unwrap(),
currency.clone(),
)
.unwrap()
.result
.try_into()
.unwrap();
assert_eq!(
query_blocks_response.blocks.len(),
std::cmp::min(
MAX_BLOCKS_PER_QUERY_BLOCK_RANGE_REQUEST as usize,
rosetta_blocks.len()
)
);

Ok(())
},
)
.unwrap();
}
}
Loading

0 comments on commit 52b5c41

Please sign in to comment.