diff --git a/documentation/api/api-analytics.yml b/documentation/api/api-analytics.yml index 2ae6a0ffe..6e329412e 100644 --- a/documentation/api/api-analytics.yml +++ b/documentation/api/api-analytics.yml @@ -140,96 +140,47 @@ paths: - $ref: "#/components/parameters/endIndex" responses: "200": - $ref: "#/components/responses/Block" + $ref: "#/components/responses/Address" "400": $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" - /api/analytics/v2/activity/blocks: + /api/analytics/v2/activity/milestones/{milestoneId}: get: tags: - activity - summary: Returns total block count. + summary: Returns milestone activity. description: >- - Returns the count of blocks created within the specified milestone range. + Returns various activity counts for a given milestone. parameters: - - $ref: "#/components/parameters/startIndex" - - $ref: "#/components/parameters/endIndex" - responses: - "200": - $ref: "#/components/responses/Block" - "400": - $ref: "#/components/responses/BadRequest" - "500": - $ref: "#/components/responses/InternalError" - /api/analytics/v2/activity/blocks/transaction: - get: - tags: - - activity - summary: Returns total transaction type block count. - description: >- - Returns the count of transaction blocks created within the specified milestone range. - parameters: - - $ref: "#/components/parameters/startIndex" - - $ref: "#/components/parameters/endIndex" - responses: - "200": - $ref: "#/components/responses/Block" - "400": - $ref: "#/components/responses/BadRequest" - "500": - $ref: "#/components/responses/InternalError" - /api/analytics/v2/activity/blocks/milestone: - get: - tags: - - activity - summary: Returns total milestone type block count. - description: >- - Returns the count of milestone blocks created within the specified milestone range. - parameters: - - $ref: "#/components/parameters/startIndex" - - $ref: "#/components/parameters/endIndex" - responses: - "200": - $ref: "#/components/responses/Block" - "400": - $ref: "#/components/responses/BadRequest" - "500": - $ref: "#/components/responses/InternalError" - /api/analytics/v2/activity/blocks/tagged-data: - get: - tags: - - activity - summary: Returns total tagged data type block count. - description: >- - Returns the count of tagged data blocks created within the specified milestone range. - parameters: - - $ref: "#/components/parameters/startIndex" - - $ref: "#/components/parameters/endIndex" + - $ref: "#/components/parameters/milestoneId" responses: "200": - $ref: "#/components/responses/Block" + $ref: "#/components/responses/Milestone" "400": $ref: "#/components/responses/BadRequest" + "404": + $ref: "#/components/responses/NoResults" "500": $ref: "#/components/responses/InternalError" - /api/analytics/v2/activity/blocks/treasury-transaction: + /api/analytics/v2/activity/milestones/by-index/{milestoneIndex}: get: tags: - activity - summary: Returns total treasury transaction type block count. + summary: Returns milestone activity. description: >- - Returns the count of treasury transaction blocks created within the specified milestone range. + Returns various activity counts for a given milestone. parameters: - - $ref: "#/components/parameters/startIndex" - - $ref: "#/components/parameters/endIndex" + - $ref: "#/components/parameters/milestoneIndex" responses: "200": - $ref: "#/components/responses/Block" + $ref: "#/components/responses/Milestone" "400": $ref: "#/components/responses/BadRequest" + "404": + $ref: "#/components/responses/NoResults" "500": - $ref: "#/components/responses/InternalError" + $ref: "#/components/responses/InternalError" /api/analytics/v2/activity/outputs: get: tags: @@ -333,14 +284,56 @@ components: - totalActiveAddresses - receivingAddresses - sendingAddresses - BlockAnalyticsResponse: - description: Compiled block statistics. + MilestoneAnalyticsResponse: + description: Compiled statistics about milestones. properties: - count: - type: string - description: The total count of blocks. + blocksCount: + type: integer + description: The number of blocks referenced by the requested milestone. + perPayloadType: + description: The various per payload type counts. + properties: + txPayloadCount: + type: integer + description: The number of transaction payloads referenced by the requested milestone. + txTreasuryPayloadCount: + type: integer + description: The number of treasury transaction payloads referenced by the requested milestone. + milestonePayloadCount: + type: integer + description: The number of milestone payloads referenced by the requested milestone. + taggedDataPayloadCount: + type: integer + description: The number of tagged data payloads referenced by the requested milestone. + noPayloadCount: + type: integer + description: The number of blocks without payload referenced by the requested milestone. + required: + - txPayloadCount + - txTreasuryPayloadCount + - milestonePayloadCount + - taggedDataPayloadCount + - noPayloadCount + perInclusionState: + description: The various per inclusion state counts. + properties: + confirmedTxCount: + type: integer + description: The number of confirmed transactions referenced by the requested milestone. + conflictingTxCount: + type: integer + description: The number of conflicting transactions referenced by the requested milestone. + noTxCount: + type: integer + description: The number of non-transaction blocks referenced by the requested milestone. + required: + - confirmedTxCount + - conflictingTxCount + - noTxCount required: - - count + - blocksCount + - perPayloadType + - perInclusionState OutputAnalyticsResponse: description: Compiled output statistics. properties: @@ -457,15 +450,6 @@ components: required: - distribution responses: - Block: - description: Successful operation. - content: - application/json: - schema: - $ref: "#/components/schemas/BlockAnalyticsResponse" - examples: - default: - $ref: "#/components/examples/block-example" Output: description: Successful operation. content: @@ -493,6 +477,15 @@ components: examples: default: $ref: "#/components/examples/storage-deposit-example" + Milestone: + description: Successful operation. + content: + application/json: + schema: + $ref: "#/components/schemas/MilestoneAnalyticsResponse" + examples: + default: + $ref: "#/components/examples/milestone-example" NoResults: description: >- Unsuccessful operation: indicates that the requested data was not found. @@ -550,6 +543,22 @@ components: example: 100 required: false description: The milestone index to be used to determine the ledger state. Defaults to 200. + milestoneId: + in: path + name: milestoneId + schema: + type: string + example: "0x7a09324557e9200f39bf493fc8fd6ac43e9ca750c6f6d884cc72386ddcb7d695" + required: true + description: Milestone id for which to receive milestone analytics. + milestoneIndex: + in: path + name: milestoneIndex + schema: + type: integer + example: 100000 + required: true + description: Milestone index for which to receive milestone analytics. examples: storage-deposit-example: value: @@ -564,9 +573,6 @@ components: - vByteCost: 500 vByteFactorData: 1 vByteFactorKey: 10 - block-example: - value: - count: "17400" output-example: value: count: "81" @@ -576,6 +582,19 @@ components: totalActiveAddresses: "443" receivingAddresses: "443" sendingAddresses: "0" + milestone-example: + value: + blocksCount: 100 + perPayloadType: + - txPayloadCount: 20 + txTreasuryPayloadCount: 2 + milestonePayloadCount: 1 + taggedDataPayloadCount: 27 + noPayloadCount: 50 + perInclusionState: + - confirmedTxCount: 20 + conflictingTxCount: 2 + noTxCount: 78 richest-addresses-example: value: top: diff --git a/src/bin/inx-chronicle/api/stardust/analytics/responses.rs b/src/bin/inx-chronicle/api/stardust/analytics/responses.rs index 000496ef3..906de126a 100644 --- a/src/bin/inx-chronicle/api/stardust/analytics/responses.rs +++ b/src/bin/inx-chronicle/api/stardust/analytics/responses.rs @@ -28,14 +28,6 @@ pub struct OutputAnalyticsResponse { impl_success_response!(OutputAnalyticsResponse); -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BlockAnalyticsResponse { - pub count: String, -} - -impl_success_response!(BlockAnalyticsResponse); - #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct StorageDepositAnalyticsResponse { @@ -102,3 +94,31 @@ impl From for DistributionStatDto { } } } + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MilestoneAnalyticsResponse { + pub blocks_count: u32, + pub per_payload_type: ActivityPerPayloadTypeDto, + pub per_inclusion_state: ActivityPerInclusionStateDto, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ActivityPerPayloadTypeDto { + pub tx_payload_count: u32, + pub treasury_tx_payload_count: u32, + pub milestone_payload_count: u32, + pub tagged_data_payload_count: u32, + pub no_payload_count: u32, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ActivityPerInclusionStateDto { + pub confirmed_tx_count: u32, + pub conflicting_tx_count: u32, + pub no_tx_count: u32, +} + +impl_success_response!(MilestoneAnalyticsResponse); diff --git a/src/bin/inx-chronicle/api/stardust/analytics/routes.rs b/src/bin/inx-chronicle/api/stardust/analytics/routes.rs index 0032e85cb..f8d97803a 100644 --- a/src/bin/inx-chronicle/api/stardust/analytics/routes.rs +++ b/src/bin/inx-chronicle/api/stardust/analytics/routes.rs @@ -1,25 +1,30 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use axum::{routing::get, Extension}; +use std::str::FromStr; + +use axum::{extract::Path, routing::get, Extension}; use bee_api_types_stardust::responses::RentStructureResponse; use chronicle::{ db::{ - collections::{BlockCollection, OutputCollection, OutputKind, PayloadKind, ProtocolUpdateCollection}, + collections::{BlockCollection, MilestoneCollection, OutputCollection, OutputKind, ProtocolUpdateCollection}, MongoDb, }, - types::stardust::block::{ - output::{AliasOutput, BasicOutput, FoundryOutput, NftOutput}, - payload::{MilestonePayload, TaggedDataPayload, TransactionPayload, TreasuryTransactionPayload}, + types::{ + stardust::block::{ + output::{AliasOutput, BasicOutput, FoundryOutput, NftOutput}, + payload::milestone::MilestoneId, + }, + tangle::MilestoneIndex, }, }; use super::{ extractors::{LedgerIndex, MilestoneRange, RichestAddressesQuery}, responses::{ - AddressAnalyticsResponse, AddressStatDto, BlockAnalyticsResponse, OutputAnalyticsResponse, - OutputDiffAnalyticsResponse, RichestAddressesResponse, StorageDepositAnalyticsResponse, - TokenDistributionResponse, + ActivityPerInclusionStateDto, ActivityPerPayloadTypeDto, AddressAnalyticsResponse, AddressStatDto, + MilestoneAnalyticsResponse, OutputAnalyticsResponse, OutputDiffAnalyticsResponse, RichestAddressesResponse, + StorageDepositAnalyticsResponse, TokenDistributionResponse, }, }; use crate::api::{error::InternalApiError, router::Router, ApiError, ApiResult}; @@ -39,20 +44,14 @@ pub fn routes() -> Router { "/activity", Router::new() .route("/addresses", get(address_activity_analytics)) - .route("/native-tokens", get(native_token_activity_analytics)) - .route("/nfts", get(nft_activity_analytics)) .nest( - "/blocks", + "/milestones", Router::new() - .route("/", get(block_activity_analytics::<()>)) - .route("/milestone", get(block_activity_analytics::)) - .route("/transaction", get(block_activity_analytics::)) - .route("/tagged-data", get(block_activity_analytics::)) - .route( - "/treasury-transaction", - get(block_activity_analytics::), - ), + .route("/:milestone_id", get(milestone_activity_analytics_by_id)) + .route("/by-index/:milestone_index", get(milestone_activity_analytics)), ) + .route("/native-tokens", get(native_token_activity_analytics)) + .route("/nfts", get(nft_activity_analytics)) .nest( "/outputs", Router::new() @@ -81,17 +80,67 @@ async fn address_activity_analytics( }) } -async fn block_activity_analytics( +async fn milestone_activity_analytics( database: Extension, - MilestoneRange { start_index, end_index }: MilestoneRange, -) -> ApiResult { - let res = database + Path(milestone_index): Path, +) -> ApiResult { + let index = MilestoneIndex::from_str(&milestone_index).map_err(ApiError::bad_parse)?; + + let activity = database .collection::() - .get_block_analytics::(start_index, end_index) + .get_milestone_activity(index) .await?; - Ok(BlockAnalyticsResponse { - count: res.count.to_string(), + Ok(MilestoneAnalyticsResponse { + blocks_count: activity.num_blocks, + per_payload_type: ActivityPerPayloadTypeDto { + tx_payload_count: activity.num_tx_payload, + treasury_tx_payload_count: activity.num_treasury_tx_payload, + tagged_data_payload_count: activity.num_tagged_data_payload, + milestone_payload_count: activity.num_milestone_payload, + no_payload_count: activity.num_no_payload, + }, + per_inclusion_state: ActivityPerInclusionStateDto { + confirmed_tx_count: activity.num_confirmed_tx, + conflicting_tx_count: activity.num_conflicting_tx, + no_tx_count: activity.num_no_tx, + }, + }) +} + +async fn milestone_activity_analytics_by_id( + database: Extension, + Path(milestone_id): Path, +) -> ApiResult { + let milestone_id = MilestoneId::from_str(&milestone_id).map_err(ApiError::bad_parse)?; + + let index = database + .collection::() + .get_milestone_payload_by_id(&milestone_id) + .await? + .ok_or(ApiError::NotFound)? + .essence + .index; + + let activity = database + .collection::() + .get_milestone_activity(index) + .await?; + + Ok(MilestoneAnalyticsResponse { + blocks_count: activity.num_blocks, + per_payload_type: ActivityPerPayloadTypeDto { + tx_payload_count: activity.num_tx_payload, + treasury_tx_payload_count: activity.num_treasury_tx_payload, + tagged_data_payload_count: activity.num_tagged_data_payload, + milestone_payload_count: activity.num_milestone_payload, + no_payload_count: activity.num_no_payload, + }, + per_inclusion_state: ActivityPerInclusionStateDto { + confirmed_tx_count: activity.num_confirmed_tx, + conflicting_tx_count: activity.num_conflicting_tx, + no_tx_count: activity.num_no_tx, + }, }) } diff --git a/src/db/collections/block.rs b/src/db/collections/block.rs index aee4572c4..b26235c88 100644 --- a/src/db/collections/block.rs +++ b/src/db/collections/block.rs @@ -11,7 +11,6 @@ use mongodb::{ use serde::{Deserialize, Serialize}; use tracing::instrument; -use super::PayloadKind; use crate::{ db::{ collections::OutputCollection, @@ -79,6 +78,19 @@ impl BlockCollection { ) .await?; + self.create_index( + IndexModel::builder() + .keys(doc! { "metadata.referenced_by_milestone_index": -1 }) + .options( + IndexOptions::builder() + .name("block_referenced_index".to_string()) + .build(), + ) + .build(), + None, + ) + .await?; + Ok(()) } @@ -247,30 +259,64 @@ impl BlockCollection { } } -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] -pub struct BlockAnalyticsResult { - pub count: u64, +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct MilestoneActivityResult { + /// The number of blocks referenced by a milestone. + pub num_blocks: u32, + /// The number of blocks referenced by a milestone that contain a payload. + pub num_tx_payload: u32, + /// The number of blocks containing a treasury transaction payload. + pub num_treasury_tx_payload: u32, + /// The number of blocks containing a milestone payload. + pub num_milestone_payload: u32, + /// The number of blocks containing a tagged data payload. + pub num_tagged_data_payload: u32, + /// The number of blocks referenced by a milestone that contain no payload. + pub num_no_payload: u32, + /// The number of blocks containing a confirmed transaction. + pub num_confirmed_tx: u32, + /// The number of blocks containing a conflicting transaction. + pub num_conflicting_tx: u32, + /// The number of blocks containing no transaction. + pub num_no_tx: u32, } impl BlockCollection { - /// Gathers block analytics. - pub async fn get_block_analytics( - &self, - start_index: Option, - end_index: Option, - ) -> Result { - let mut queries = vec![doc! { - "$nor": [ - { "metadata.referenced_by_milestone_index": { "$lt": start_index } }, - { "metadata.referenced_by_milestone_index": { "$gte": end_index } }, - ], - }]; - if let Some(kind) = B::kind() { - queries.push(doc! { "block.payload.kind": kind }); - } + /// Gathers past-cone activity statistics for a given milestone. + pub async fn get_milestone_activity(&self, index: MilestoneIndex) -> Result { Ok(self .aggregate( - vec![doc! { "$match": { "$and": queries } }, doc! { "$count": "count" }], + vec![ + doc! { "$match": { "metadata.referenced_by_milestone_index": index } }, + doc! { "$group": { + "_id": null, + "num_blocks": { "$count": {} }, + "num_tx_payload": { "$sum": { + "$cond": [ { "$eq": [ "$block.payload.kind", "transaction" ] }, 1 , 0 ] + } }, + "num_treasury_tx_payload": { "$sum": { + "$cond": [ { "$eq": [ "$block.payload.kind", "treasury_transaction" ] }, 1 , 0 ] + } }, + "num_milestone_payload": { "$sum": { + "$cond": [ { "$eq": [ "$block.payload.kind", "milestone" ] }, 1 , 0 ] + } }, + "num_tagged_data_payload": { "$sum": { + "$cond": [ { "$eq": [ "$block.payload.kind", "tagged_data" ] }, 1 , 0 ] + } }, + "num_no_payload": { "$sum": { + "$cond": [ { "$not": "$block.payload" }, 1 , 0 ] + } }, + "num_confirmed_tx": { "$sum": { + "$cond": [ { "$eq": [ "$metadata.inclusion_state", "included" ] }, 1 , 0 ] + } }, + "num_conflicting_tx": { "$sum": { + "$cond": [ { "$eq": [ "$metadata.inclusion_state", "conflicting" ] }, 1 , 0 ] + } }, + "num_no_tx": { "$sum": { + "$cond": [ { "$eq": [ "$metadata.inclusion_state", "no_transaction" ] }, 1 , 0 ] + } }, + } }, + ], None, ) .await? diff --git a/src/types/stardust/util/mod.rs b/src/types/stardust/util/mod.rs index 55922a4c2..6ec245d91 100644 --- a/src/types/stardust/util/mod.rs +++ b/src/types/stardust/util/mod.rs @@ -41,3 +41,12 @@ pub fn get_test_tagged_data_block() -> Block { .unwrap(), ) } + +pub fn get_test_no_payload_block() -> Block { + Block::from( + bee::BlockBuilder::::new(bee_block_stardust::rand::parents::rand_parents()) + .with_nonce_provider(u64::MAX, 0) + .finish() + .unwrap(), + ) +} diff --git a/tests/blocks.rs b/tests/blocks.rs index 5b52a91f2..8e5febe32 100644 --- a/tests/blocks.rs +++ b/tests/blocks.rs @@ -119,3 +119,67 @@ async fn test_blocks() { db.drop().await.unwrap(); } + +#[tokio::test] +async fn test_milestone_activity() { + let db = connect_to_test_db("test-milestone-activity").await.unwrap(); + db.clear().await.unwrap(); + let collection = db.collection::(); + collection.create_indexes().await.unwrap(); + + // Note that we cannot build a block with a treasury transaction payload. + let blocks = vec![ + get_test_transaction_block(), + get_test_transaction_block(), + get_test_milestone_block(), + get_test_tagged_data_block(), + get_test_no_payload_block(), + ] + .into_iter() + .enumerate() + .map(|(i, block)| { + let bee_block = bee::Block::try_from(block.clone()).unwrap(); + let parents = block.parents.clone(); + ( + bee_block.id().into(), + block, + bee_block.pack_to_vec(), + BlockMetadata { + parents, + is_solid: true, + should_promote: false, + should_reattach: false, + referenced_by_milestone_index: 1.into(), + milestone_index: 0.into(), + inclusion_state: match i { + 0 => LedgerInclusionState::Included, + 1 => LedgerInclusionState::Conflicting, + _ => LedgerInclusionState::NoTransaction, + }, + conflict_reason: match i { + 0 => ConflictReason::None, + 1 => ConflictReason::InputUtxoNotFound, + _ => ConflictReason::None, + }, + white_flag_index: i as u32, + }, + ) + }) + .collect::>(); + + collection.insert_blocks_with_metadata(blocks.clone()).await.unwrap(); + + let activity = collection.get_milestone_activity(1.into()).await.unwrap(); + + assert_eq!(activity.num_blocks, 5); + assert_eq!(activity.num_tx_payload, 2); + assert_eq!(activity.num_treasury_tx_payload, 0); + assert_eq!(activity.num_milestone_payload, 1); + assert_eq!(activity.num_tagged_data_payload, 1); + assert_eq!(activity.num_no_payload, 1); + assert_eq!(activity.num_confirmed_tx, 1); + assert_eq!(activity.num_conflicting_tx, 1); + assert_eq!(activity.num_no_tx, 3); + + db.drop().await.unwrap(); +}