From 09083e5420480d01eae0d12652c48f4a552873f8 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Thu, 11 Sep 2025 11:44:47 +0300 Subject: [PATCH 1/4] API server: add an endpoint to return block infos that include the block difficulty target --- Cargo.lock | 3 + api-server/api-server-common/src/lib.rs | 1 + .../src/storage/impls/in_memory/mod.rs | 44 +++- .../impls/in_memory/transactional/read.rs | 8 + .../impls/in_memory/transactional/write.rs | 8 + .../src/storage/impls/mod.rs | 2 +- .../src/storage/impls/postgres/queries.rs | 104 +++++++-- .../impls/postgres/transactional/read.rs | 11 + .../impls/postgres/transactional/write.rs | 11 + .../src/storage/storage_api/block_aux_data.rs | 9 +- .../src/storage/storage_api/mod.rs | 10 + api-server/api-server-common/src/utils.rs | 40 ++++ .../scanner-lib/src/blockchain_state/mod.rs | 20 +- api-server/stack-test-suite/Cargo.toml | 5 +- api-server/stack-test-suite/tests/v2/chain.rs | 216 ++++++++++++++++++ .../stack-test-suite/tests/v2/chain_tip.rs | 41 ++-- .../stack-test-suite/tests/v2/feerate.rs | 2 +- api-server/stack-test-suite/tests/v2/htlc.rs | 3 +- api-server/stack-test-suite/tests/v2/mod.rs | 7 + .../stack-test-suite/tests/v2/transaction.rs | 52 ++--- .../stack-test-suite/tests/v2/transactions.rs | 9 +- api-server/stack-test-suite/tests/v2/utils.rs | 76 ++++++ api-server/storage-test-suite/src/basic.rs | 46 ++-- api-server/web-server/src/api/json_helpers.rs | 25 +- api-server/web-server/src/api/v2.rs | 156 ++++++++++--- chainstate/test-framework/Cargo.toml | 1 + chainstate/test-framework/src/framework.rs | 100 ++++++-- chainstate/test-framework/src/lib.rs | 11 +- .../test-framework/src/pos_block_builder.rs | 46 ++-- .../src/test_block_index_handle.rs} | 0 chainstate/test-framework/src/utils.rs | 55 ++++- .../test-suite/src/tests/helpers/mod.rs | 1 - .../test-suite/src/tests/helpers/pos.rs | 30 +-- .../test-suite/src/tests/history_iteration.rs | 5 +- common/src/primitives/compact.rs | 4 +- 35 files changed, 936 insertions(+), 226 deletions(-) create mode 100644 api-server/api-server-common/src/utils.rs create mode 100644 api-server/stack-test-suite/tests/v2/chain.rs create mode 100644 api-server/stack-test-suite/tests/v2/utils.rs rename chainstate/{test-suite/src/tests/helpers/block_index_handle_impl.rs => test-framework/src/test_block_index_handle.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index 51251b6c1c..171a2652ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,8 +349,11 @@ dependencies = [ "chainstate-test-framework", "common", "crypto", + "ctor", "hex", + "itertools 0.14.0", "libtest-mimic", + "logging", "mempool", "node-comm", "pos-accounting", diff --git a/api-server/api-server-common/src/lib.rs b/api-server/api-server-common/src/lib.rs index 9d979da77a..7c47a17b24 100644 --- a/api-server/api-server-common/src/lib.rs +++ b/api-server/api-server-common/src/lib.rs @@ -14,6 +14,7 @@ // limitations under the License. pub mod storage; +pub mod utils; use clap::Parser; use common::chain::config::ChainType; diff --git a/api-server/api-server-common/src/storage/impls/in_memory/mod.rs b/api-server/api-server-common/src/storage/impls/in_memory/mod.rs index ce2258bfb3..5396efcfed 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/mod.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/mod.rs @@ -15,11 +15,14 @@ pub mod transactional; -use crate::storage::storage_api::{ - block_aux_data::{BlockAuxData, BlockWithExtraData}, - AmountWithDecimals, ApiServerStorageError, BlockInfo, CoinOrTokenStatistic, Delegation, - FungibleTokenData, LockedUtxo, NftWithOwner, Order, PoolBlockStats, PoolDataWithExtraInfo, - TransactionInfo, TransactionWithBlockInfo, Utxo, UtxoLock, UtxoWithExtraInfo, +use crate::{ + storage::storage_api::{ + block_aux_data::{BlockAuxData, BlockWithExtraData}, + AmountWithDecimals, ApiServerStorageError, BlockInfo, CoinOrTokenStatistic, Delegation, + FungibleTokenData, LockedUtxo, NftWithOwner, Order, PoolBlockStats, PoolDataWithExtraInfo, + TransactionInfo, TransactionWithBlockInfo, Utxo, UtxoLock, UtxoWithExtraInfo, + }, + utils::get_block_compact_target, }; use common::{ address::Address, @@ -93,6 +96,7 @@ impl ApiServerInMemoryStorage { chain_config.genesis_block_id(), 0.into(), chain_config.genesis_block().timestamp(), + None, ), number_of_coin_decimals: chain_config.coin_decimals(), storage_version: super::CURRENT_STORAGE_VERSION, @@ -327,6 +331,23 @@ impl ApiServerInMemoryStorage { Ok(Some(*block_aux_data)) } + fn get_blocks_aux_data( + &self, + blocks_count: u32, + starting_height: u64, + ) -> Result, ApiServerStorageError> { + let start = BlockHeight::new(starting_height); + let end = BlockHeight::new(starting_height + blocks_count as u64); + self.main_chain_blocks_table + .range(start..end) + .map(|(_, block_id)| { + Ok(*self.block_aux_data_table.get(block_id).ok_or( + ApiServerStorageError::AuxDataMissingForMainchainBlock(*block_id), + )?) + }) + .collect() + } + fn get_block_range_from_time_range( &self, time_range: (BlockTimestamp, BlockTimestamp), @@ -971,13 +992,16 @@ impl ApiServerInMemoryStorage { block_height: BlockHeight, block: &BlockWithExtraData, ) -> Result<(), ApiServerStorageError> { - self.block_table.insert(block_id, block.clone()); - self.block_aux_data_table.insert( - block_id, - BlockAuxData::new(block_id.into(), block_height, block.block.timestamp()), + let data = BlockAuxData::new( + block_id.into(), + block_height, + block.block.timestamp(), + get_block_compact_target(&block.block), ); + self.block_table.insert(block_id, block.clone()); + self.block_aux_data_table.insert(block_id, data); self.main_chain_blocks_table.insert(block_height, block_id); - self.best_block = BlockAuxData::new(block_id.into(), block_height, block.block.timestamp()); + self.best_block = data; Ok(()) } diff --git a/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs b/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs index 7d0b1729d6..0174a22c97 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs @@ -178,6 +178,14 @@ impl ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRo<'_> { self.transaction.get_block_aux_data(block_id) } + async fn get_blocks_aux_data( + &self, + blocks_count: u32, + starting_height: u64, + ) -> Result, ApiServerStorageError> { + self.transaction.get_blocks_aux_data(blocks_count, starting_height) + } + async fn get_main_chain_block_id( &self, block_height: BlockHeight, diff --git a/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs b/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs index b8dc530c3f..db08dfb194 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs @@ -355,6 +355,14 @@ impl ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRw<'_> { self.transaction.get_block_aux_data(block_id) } + async fn get_blocks_aux_data( + &self, + blocks_count: u32, + starting_height: u64, + ) -> Result, ApiServerStorageError> { + self.transaction.get_blocks_aux_data(blocks_count, starting_height) + } + async fn get_block_range_from_time_range( &self, time_range: (BlockTimestamp, BlockTimestamp), diff --git a/api-server/api-server-common/src/storage/impls/mod.rs b/api-server/api-server-common/src/storage/impls/mod.rs index 4e3c9c5041..a912242875 100644 --- a/api-server/api-server-common/src/storage/impls/mod.rs +++ b/api-server/api-server-common/src/storage/impls/mod.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub const CURRENT_STORAGE_VERSION: u32 = 22; +pub const CURRENT_STORAGE_VERSION: u32 = 23; pub mod in_memory; pub mod postgres; diff --git a/api-server/api-server-common/src/storage/impls/postgres/queries.rs b/api-server/api-server-common/src/storage/impls/postgres/queries.rs index b98c4eeb49..0100240390 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/queries.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/queries.rs @@ -29,18 +29,22 @@ use common::{ AccountNonce, Block, ChainConfig, DelegationId, Destination, GenBlock, OrderId, PoolId, Transaction, UtxoOutPoint, }, - primitives::{Amount, BlockHeight, CoinOrTokenId, Id}, + primitives::{compact, Amount, BlockHeight, CoinOrTokenId, Compact, Id}, }; use tokio_postgres::NoTls; -use crate::storage::{ - impls::CURRENT_STORAGE_VERSION, - storage_api::{ - block_aux_data::{BlockAuxData, BlockWithExtraData}, - AmountWithDecimals, ApiServerStorageError, BlockInfo, CoinOrTokenStatistic, Delegation, - FungibleTokenData, LockedUtxo, NftWithOwner, Order, PoolBlockStats, PoolDataWithExtraInfo, - TransactionInfo, TransactionWithBlockInfo, Utxo, UtxoWithExtraInfo, +use crate::{ + storage::{ + impls::CURRENT_STORAGE_VERSION, + storage_api::{ + block_aux_data::{BlockAuxData, BlockWithExtraData}, + AmountWithDecimals, ApiServerStorageError, BlockInfo, CoinOrTokenStatistic, Delegation, + FungibleTokenData, LockedUtxo, NftWithOwner, Order, PoolBlockStats, + PoolDataWithExtraInfo, TransactionInfo, TransactionWithBlockInfo, Utxo, + UtxoWithExtraInfo, + }, }, + utils::get_block_compact_target, }; const VERSION_STR: &str = "version"; @@ -588,7 +592,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { .query_one( r#" ( - SELECT block_height, block_id, block_timestamp + SELECT block_height, block_id, block_timestamp, block_compact_target FROM ml.blocks WHERE block_height IS NOT NULL ORDER BY block_height DESC @@ -596,7 +600,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { ) UNION ALL ( - SELECT block_height, block_id, block_timestamp + SELECT block_height, block_id, block_timestamp, null FROM ml.genesis LIMIT 1 ) @@ -611,6 +615,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { let block_height: i64 = row.get(0); let block_id: Vec = row.get(1); let block_timestamp: i64 = row.get(2); + let block_compact_target: Option = row.get(3); let block_height = BlockHeight::new(block_height as u64); let block_timestamp = BlockTimestamp::from_int_seconds(block_timestamp as u64); @@ -620,8 +625,19 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { e )) })?; + let block_compact_target: Option = block_compact_target + .map(|val| { + val.try_into() + .map_err(|_| ApiServerStorageError::UnexpectedCompactTargetInDb(val)) + }) + .transpose()?; - Ok(BlockAuxData::new(block_id, block_height, block_timestamp)) + Ok(BlockAuxData::new( + block_id, + block_height, + block_timestamp, + block_compact_target.map(Compact), + )) } async fn just_execute(&mut self, query: &str) -> Result<(), ApiServerStorageError> { @@ -661,6 +677,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { block_id bytea PRIMARY KEY, block_height bigint, block_timestamp bigint NOT NULL, + block_compact_target bigint, block_data bytea NOT NULL );", ) @@ -1133,13 +1150,15 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { logging::log::debug!("Inserting block with id: {:?}", block_id); let height = Self::block_height_to_postgres_friendly(block_height); let timestamp = Self::block_time_to_postgres_friendly(block.block.timestamp())?; + let compact_target = get_block_compact_target(&block.block).map(|target| target.0 as i64); self.tx .execute( - "INSERT INTO ml.blocks (block_id, block_height, block_timestamp, block_data) VALUES ($1, $2, $3, $4) + "INSERT INTO ml.blocks (block_id, block_height, block_timestamp, block_compact_target, block_data) + VALUES ($1, $2, $3, $4, $5) ON CONFLICT (block_id) DO UPDATE - SET block_data = $4, block_height = $2;", - &[&block_id.encode(), &height, ×tamp, &block.encode()], + SET block_data = $5, block_height = $2;", + &[&block_id.encode(), &height, ×tamp, &compact_target, &block.encode()], ) .await .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?; @@ -2698,6 +2717,63 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { Ok(Some(block_aux_data)) } + pub async fn get_blocks_aux_data( + &mut self, + blocks_count: u32, + starting_height: u64, + ) -> Result, ApiServerStorageError> { + let blocks_count = blocks_count as i64; + let starting_height = starting_height as i64; + let rows = self + .tx + .query( + // Note: using OFFSET becomes really slow when the offset value is large, + // this is why we use a starting height instead. + r#" + SELECT block_height, block_id, block_timestamp, block_compact_target + FROM ml.blocks + WHERE block_height IS NOT NULL AND block_height >= $1 + ORDER BY block_height + LIMIT $2; + "#, + &[&starting_height, &blocks_count], + ) + .await + .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?; + + rows.into_iter() + .map(|row| { + let block_height: i64 = row.get(0); + let block_id: Vec = row.get(1); + let block_timestamp: i64 = row.get(2); + let block_compact_target: Option = row.get(3); + + let block_height = BlockHeight::new(block_height as u64); + let block_timestamp = BlockTimestamp::from_int_seconds(block_timestamp as u64); + let block_id = + Id::::decode_all(&mut block_id.as_slice()).map_err(|e| { + ApiServerStorageError::InvalidInitializedState(format!( + "BlockId deserialization failed: {}", + e + )) + })?; + let block_compact_target: Option = block_compact_target + .map(|val| { + val.try_into() + .map_err(|_| ApiServerStorageError::UnexpectedCompactTargetInDb(val)) + }) + .transpose()?; + + Ok(BlockAuxData::new( + block_id, + block_height, + block_timestamp, + block_compact_target.map(Compact), + )) + }) + .collect() + } + pub async fn set_block_aux_data( &mut self, block_id: Id, diff --git a/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs b/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs index 3a17e183c9..3727883804 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs @@ -133,6 +133,17 @@ impl ApiServerStorageRead for ApiServerPostgresTransactionalRo<'_> { Ok(res) } + async fn get_blocks_aux_data( + &self, + blocks_count: u32, + starting_height: u64, + ) -> Result, ApiServerStorageError> { + let mut conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); + let res = conn.get_blocks_aux_data(blocks_count, starting_height).await?; + + Ok(res) + } + async fn get_block_range_from_time_range( &self, time_range: (BlockTimestamp, BlockTimestamp), diff --git a/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs b/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs index 7f5549ef9f..cf74a13e10 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs @@ -470,6 +470,17 @@ impl ApiServerStorageRead for ApiServerPostgresTransactionalRw<'_> { Ok(res) } + async fn get_blocks_aux_data( + &self, + blocks_count: u32, + starting_height: u64, + ) -> Result, ApiServerStorageError> { + let mut conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); + let res = conn.get_blocks_aux_data(blocks_count, starting_height).await?; + + Ok(res) + } + async fn get_block_range_from_time_range( &self, time_range: (BlockTimestamp, BlockTimestamp), diff --git a/api-server/api-server-common/src/storage/storage_api/block_aux_data.rs b/api-server/api-server-common/src/storage/storage_api/block_aux_data.rs index 992258fa66..86bd0d1a39 100644 --- a/api-server/api-server-common/src/storage/storage_api/block_aux_data.rs +++ b/api-server/api-server-common/src/storage/storage_api/block_aux_data.rs @@ -15,7 +15,7 @@ use common::{ chain::{block::timestamp::BlockTimestamp, Block, GenBlock}, - primitives::{BlockHeight, Id}, + primitives::{BlockHeight, Compact, Id}, }; use serialization::{Decode, Encode}; @@ -26,6 +26,7 @@ pub struct BlockAuxData { block_id: Id, block_height: BlockHeight, block_timestamp: BlockTimestamp, + block_compact_target: Option, } impl BlockAuxData { @@ -33,11 +34,13 @@ impl BlockAuxData { block_id: Id, block_height: BlockHeight, block_timestamp: BlockTimestamp, + block_compact_target: Option, ) -> Self { Self { block_id, block_height, block_timestamp, + block_compact_target, } } @@ -52,6 +55,10 @@ impl BlockAuxData { pub fn block_timestamp(&self) -> BlockTimestamp { self.block_timestamp } + + pub fn block_compact_target(&self) -> Option { + self.block_compact_target + } } #[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)] diff --git a/api-server/api-server-common/src/storage/storage_api/mod.rs b/api-server/api-server-common/src/storage/storage_api/mod.rs index d93afa9a81..dafb203af3 100644 --- a/api-server/api-server-common/src/storage/storage_api/mod.rs +++ b/api-server/api-server-common/src/storage/storage_api/mod.rs @@ -70,6 +70,10 @@ pub enum ApiServerStorageError { TimestampTooHigh(BlockTimestamp), #[error("Id creation error: {0}")] IdCreationError(#[from] IdCreationError), + #[error("Unexpected compact target {0:?} stored in the db")] + UnexpectedCompactTargetInDb(i64), + #[error("Aux data missing for mainchain block {0:x}")] + AuxDataMissingForMainchainBlock(Id), } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] @@ -611,6 +615,12 @@ pub trait ApiServerStorageRead: Sync { block_id: Id, ) -> Result, ApiServerStorageError>; + async fn get_blocks_aux_data( + &self, + blocks_count: u32, + starting_height: u64, + ) -> Result, ApiServerStorageError>; + async fn get_block_range_from_time_range( &self, time_range: (BlockTimestamp, BlockTimestamp), diff --git a/api-server/api-server-common/src/utils.rs b/api-server/api-server-common/src/utils.rs new file mode 100644 index 0000000000..f97021b3a5 --- /dev/null +++ b/api-server/api-server-common/src/utils.rs @@ -0,0 +1,40 @@ +// Copyright (c) 2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use common::{ + chain::{block::ConsensusData, Block}, + primitives::Compact, + Uint256, +}; + +pub fn get_block_compact_target(block: &Block) -> Option { + match block.consensus_data() { + ConsensusData::None => None, + ConsensusData::PoW(data) => Some(data.bits()), + ConsensusData::PoS(data) => Some(data.compact_target()), + } +} + +pub fn unpack_block_compact_target( + compact_target: Compact, +) -> Result { + let target = Uint256::try_from(compact_target) + .map_err(|_| BlockCompactTargetUnpackingError(compact_target))?; + Ok(target) +} + +#[derive(Debug, Eq, PartialEq, thiserror::Error)] +#[error("Compact target {0:?} cannot be converted to an integer")] +pub struct BlockCompactTargetUnpackingError(Compact); diff --git a/api-server/scanner-lib/src/blockchain_state/mod.rs b/api-server/scanner-lib/src/blockchain_state/mod.rs index 1955af4262..f41db88d7c 100644 --- a/api-server/scanner-lib/src/blockchain_state/mod.rs +++ b/api-server/scanner-lib/src/blockchain_state/mod.rs @@ -14,11 +14,14 @@ // limitations under the License. use crate::sync::local_state::LocalBlockchainState; -use api_server_common::storage::storage_api::{ - block_aux_data::{BlockAuxData, BlockWithExtraData}, - ApiServerStorage, ApiServerStorageError, ApiServerStorageRead, ApiServerStorageWrite, - ApiServerTransactionRw, CoinOrTokenStatistic, Delegation, FungibleTokenData, LockedUtxo, Order, - PoolDataWithExtraInfo, TransactionInfo, TxAdditionalInfo, Utxo, UtxoLock, +use api_server_common::{ + storage::storage_api::{ + block_aux_data::{BlockAuxData, BlockWithExtraData}, + ApiServerStorage, ApiServerStorageError, ApiServerStorageRead, ApiServerStorageWrite, + ApiServerTransactionRw, CoinOrTokenStatistic, Delegation, FungibleTokenData, LockedUtxo, + Order, PoolDataWithExtraInfo, TransactionInfo, TxAdditionalInfo, Utxo, UtxoLock, + }, + utils::get_block_compact_target, }; use chainstate::{ calculate_median_time_past_from_blocktimestamps, @@ -208,7 +211,12 @@ impl LocalBlockchainState for BlockchainState db_tx .set_block_aux_data( block_id, - &BlockAuxData::new(block_id.into(), block_height, block_timestamp), + &BlockAuxData::new( + block_id.into(), + block_height, + block_timestamp, + get_block_compact_target(&block), + ), ) .await .expect("Unable to set block aux data"); diff --git a/api-server/stack-test-suite/Cargo.toml b/api-server/stack-test-suite/Cargo.toml index 0d120a968d..435d88337a 100644 --- a/api-server/stack-test-suite/Cargo.toml +++ b/api-server/stack-test-suite/Cargo.toml @@ -13,6 +13,7 @@ chainstate = { path = "../../chainstate" } chainstate-test-framework = { path = "../../chainstate/test-framework" } common = { path = "../../common" } crypto = { path = "../../crypto" } +logging = { path = "../../logging" } randomness = { path = "../../randomness" } serialization = { path = "../../serialization" } test-utils = { path = "../../test-utils" } @@ -23,10 +24,12 @@ mempool = { path = "../../mempool" } async-trait.workspace = true axum.workspace = true +ctor.workspace = true hex.workspace = true +itertools.workspace = true libtest-mimic.workspace = true reqwest = "0.11" +rstest.workspace = true serde.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["full"] } -rstest.workspace = true diff --git a/api-server/stack-test-suite/tests/v2/chain.rs b/api-server/stack-test-suite/tests/v2/chain.rs new file mode 100644 index 0000000000..631caf5f7d --- /dev/null +++ b/api-server/stack-test-suite/tests/v2/chain.rs @@ -0,0 +1,216 @@ +// Copyright (c) 2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::RwLock; + +use itertools::Itertools as _; + +use api_web_server::{ + api::v2::{DEFAULT_NUM_ITEMS, MAX_NUM_ITEMS}, + CachedValues, +}; +use chainstate_test_framework::get_pos_target; +use common::primitives::time::get_time; +use test_utils::assert_matches_return_val; + +use crate::{v2::utils::create_chain, DummyRPC}; + +use super::*; + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test] +async fn ok(#[case] seed: Seed, #[values(false, true)] use_pos: bool) { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let mut rng = make_seedable_rng(seed); + + let (tx, rx) = tokio::sync::oneshot::channel(); + let inner_rng_seed = rng.gen(); + + let task = tokio::spawn({ + async move { + let mut rng = make_seedable_rng(inner_rng_seed); + let blocks_count = rng.gen_range(10..100); + + let web_server_state = { + let (chain_config, chainstate_blocks) = + create_chain(blocks_count, use_pos, &mut rng); + let genesis_id = chain_config.genesis_block().get_id(); + let genesis_timestamp = chain_config.genesis_block().timestamp(); + + let expected_jsons = std::iter::once(json!({ + "block_height": 0, + "block_id": genesis_id.to_hash().encode_hex::(), + "target": null, + "timestamp": genesis_timestamp.as_int_seconds() + })) + .chain(chainstate_blocks.iter().enumerate().map(|(idx, block)| { + let expected_target = use_pos.then(|| { + let target = get_pos_target(block).unwrap(); + format!("0x{target:x}") + }); + json!({ + "block_height": idx + 1, + "block_id": block.get_id(), + "target": expected_target, + "timestamp": block.timestamp().as_int_seconds() + }) + })) + .collect_vec(); + + _ = tx.send(expected_jsons); + + let storage = { + let mut storage = TransactionalApiServerInMemoryStorage::new(&chain_config); + + let mut db_tx = storage.transaction_rw().await.unwrap(); + db_tx.reinitialize_storage(&chain_config).await.unwrap(); + db_tx.commit().await.unwrap(); + + storage + }; + + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); + local_node.scan_genesis(chain_config.genesis_block()).await.unwrap(); + local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); + + ApiServerWebServerState { + db: Arc::new(local_node.storage().clone_storage().await), + chain_config: Arc::clone(&chain_config), + rpc: Arc::new(DummyRPC {}), + cached_values: Arc::new(CachedValues { + feerate_points: RwLock::new((get_time(), vec![])), + }), + time_getter: Default::default(), + } + }; + + web_server(listener, web_server_state, true).await + } + }); + + let all_expected_jsons = rx.await.unwrap(); + // Total blocks count, including genesis. + let total_blocks_count = all_expected_jsons.len(); + + // Request all blocks + { + let url = format!("/api/v2/chain?offset=0&items={total_blocks_count}"); + let response = reqwest::get(format!("http://{}:{}{url}", addr.ip(), addr.port())) + .await + .unwrap(); + assert_eq!(response.status(), 200); + + let response = array_from_response(response).await; + assert_eq!(response, all_expected_jsons); + } + + // Request only the genesis + { + let url = "/api/v2/chain?offset=0&items=1"; + let response = reqwest::get(format!("http://{}:{}{url}", addr.ip(), addr.port())) + .await + .unwrap(); + assert_eq!(response.status(), 200); + + let response = array_from_response(response).await; + assert_eq!(&response, &all_expected_jsons[..1]); + } + + // Request without params + { + let url = "/api/v2/chain"; + let response = reqwest::get(format!("http://{}:{}{url}", addr.ip(), addr.port())) + .await + .unwrap(); + assert_eq!(response.status(), 200); + + let response = array_from_response(response).await; + assert_eq!(&response, &all_expected_jsons[..DEFAULT_NUM_ITEMS as usize]); + } + + // Request random number of blocks + { + let offset = rng.gen_range(0..=total_blocks_count); + let items = rng.gen_range(0..=total_blocks_count); + let url = format!("/api/v2/chain?offset={offset}&items={items}"); + let response = reqwest::get(format!("http://{}:{}{url}", addr.ip(), addr.port())) + .await + .unwrap(); + assert_eq!(response.status(), 200); + + let response = array_from_response(response).await; + let end_idx = std::cmp::min(offset + items, all_expected_jsons.len()); + assert_eq!(&response, &all_expected_jsons[offset..end_idx]); + } + + task.abort(); +} + +#[tokio::test] +async fn invalid_offset() { + let (task, response) = spawn_webserver("/api/v2/chain?offset=asd").await; + + assert_eq!(response.status(), 400); + + let body = response.text().await.unwrap(); + let body: serde_json::Value = serde_json::from_str(&body).unwrap(); + + assert_eq!(body["error"].as_str().unwrap(), "Invalid offset"); + + task.abort(); +} + +#[tokio::test] +async fn invalid_num_items() { + let (task, response) = spawn_webserver("/api/v2/chain?items=asd").await; + + assert_eq!(response.status(), 400); + + let body = response.text().await.unwrap(); + let body: serde_json::Value = serde_json::from_str(&body).unwrap(); + + assert_eq!(body["error"].as_str().unwrap(), "Invalid number of items"); + + task.abort(); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test] +async fn invalid_num_items_max(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let more_than_max = rng.gen_range(MAX_NUM_ITEMS + 1..MAX_NUM_ITEMS * 2); + let (task, response) = spawn_webserver(&format!("/api/v2/chain?items={more_than_max}")).await; + + assert_eq!(response.status(), 400); + + let body = response.text().await.unwrap(); + let body: serde_json::Value = serde_json::from_str(&body).unwrap(); + + assert_eq!(body["error"].as_str().unwrap(), "Invalid number of items"); + + task.abort(); +} + +async fn array_from_response(response: reqwest::Response) -> Vec { + let body = response.text().await.unwrap(); + let body: serde_json::Value = serde_json::from_str(&body).unwrap(); + assert_matches_return_val!(body, serde_json::Value::Array(array), array) +} diff --git a/api-server/stack-test-suite/tests/v2/chain_tip.rs b/api-server/stack-test-suite/tests/v2/chain_tip.rs index 3983c1b58b..c764d3c3ee 100644 --- a/api-server/stack-test-suite/tests/v2/chain_tip.rs +++ b/api-server/stack-test-suite/tests/v2/chain_tip.rs @@ -16,9 +16,10 @@ use std::sync::RwLock; use api_web_server::CachedValues; +use chainstate_test_framework::get_pos_target; use common::primitives::time::get_time; -use crate::DummyRPC; +use crate::{v2::utils::create_chain, DummyRPC}; use super::*; @@ -37,10 +38,13 @@ async fn at_genesis() { let chain_config = Arc::new(create_unit_test_config()); let binding = Arc::clone(&chain_config); let expected_genesis_id = binding.genesis_block().get_id(); + let expected_genesis_timestamp = binding.genesis_block().timestamp(); _ = tx.send(json!({ "block_height": 0, "block_id": expected_genesis_id.to_hash().encode_hex::(), + "target": null, + "timestamp": expected_genesis_timestamp.as_int_seconds() })); let storage = TransactionalApiServerInMemoryStorage::new(&chain_config); @@ -83,7 +87,7 @@ async fn at_genesis() { #[trace] #[case(Seed::from_entropy())] #[tokio::test] -async fn height_n(#[case] seed: Seed) { +async fn height_n(#[case] seed: Seed, #[values(false, true)] use_pos: bool) { let url = "/api/v2/chain/tip"; let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -97,30 +101,21 @@ async fn height_n(#[case] seed: Seed) { let n_blocks = rng.gen_range(1..100); let web_server_state = { - let chain_config = create_unit_test_config(); + let (chain_config, chainstate_blocks) = create_chain(n_blocks, use_pos, &mut rng); - let chainstate_blocks = { - let mut tf = TestFramework::builder(&mut rng) - .with_chain_config(chain_config.clone()) - .build(); + // Need the "- 1" to account for the genesis block not in the vec + let expected_block = &chainstate_blocks[n_blocks - 1]; + let expected_target = use_pos.then(|| { + let target = get_pos_target(expected_block).unwrap(); + format!("0x{target:x}") + }); - let chainstate_block_ids = tf - .create_chain_return_ids(&tf.genesis().get_id().into(), n_blocks, &mut rng) - .unwrap(); - - // Need the "- 1" to account for the genesis block not in the vec - let expected_block_id = chainstate_block_ids[n_blocks - 1]; - - _ = tx.send(json!({ + _ = tx.send(json!({ "block_height": n_blocks, - "block_id": expected_block_id, - })); - - chainstate_block_ids - .iter() - .map(|id| tf.block(tf.to_chain_block_id(id))) - .collect::>() - }; + "block_id": expected_block.get_id(), + "target": expected_target, + "timestamp": expected_block.timestamp().as_int_seconds() + })); let storage = { let mut storage = TransactionalApiServerInMemoryStorage::new(&chain_config); diff --git a/api-server/stack-test-suite/tests/v2/feerate.rs b/api-server/stack-test-suite/tests/v2/feerate.rs index 144f8c4292..64a1e679ea 100644 --- a/api-server/stack-test-suite/tests/v2/feerate.rs +++ b/api-server/stack-test-suite/tests/v2/feerate.rs @@ -15,12 +15,12 @@ use std::sync::RwLock; +use ::utils::atomics::SeqCstAtomicU64; use api_web_server::{CachedValues, TxSubmitClient}; use common::primitives::time::get_time; use mempool::FeeRate; use node_comm::rpc_client::NodeRpcError; use test_utils::mock_time_getter::mocked_time_getter_seconds; -use utils::atomics::SeqCstAtomicU64; use super::*; diff --git a/api-server/stack-test-suite/tests/v2/htlc.rs b/api-server/stack-test-suite/tests/v2/htlc.rs index 781328c03d..2ea4f9dab1 100644 --- a/api-server/stack-test-suite/tests/v2/htlc.rs +++ b/api-server/stack-test-suite/tests/v2/htlc.rs @@ -15,6 +15,7 @@ use std::borrow::Cow; +use ::utils::const_nz_u8; use common::chain::{ classic_multisig::ClassicMultisigChallenge, htlc::HtlcSecret, @@ -44,7 +45,7 @@ fn create_htlc( ) -> (HashedTimelockContract, ClassicMultisigChallenge) { let refund_challenge = ClassicMultisigChallenge::new( chain_config, - utils::const_nz_u8!(2), + const_nz_u8!(2), vec![alice_pk.clone(), bob_pk.clone()], ) .unwrap(); diff --git a/api-server/stack-test-suite/tests/v2/mod.rs b/api-server/stack-test-suite/tests/v2/mod.rs index f80b77305d..e6ee90afcc 100644 --- a/api-server/stack-test-suite/tests/v2/mod.rs +++ b/api-server/stack-test-suite/tests/v2/mod.rs @@ -22,6 +22,7 @@ mod block; mod block_header; mod block_reward; mod block_transaction_ids; +mod chain; mod chain_at_height; mod chain_tip; mod feerate; @@ -40,6 +41,7 @@ mod transaction; mod transaction_merkle_path; mod transaction_submit; mod transactions; +mod utils; use crate::{spawn_webserver, DummyRPC}; use api_blockchain_scanner_lib::{ @@ -82,6 +84,11 @@ use std::{ }; use test_utils::random::{make_seedable_rng, Rng, Seed}; +#[ctor::ctor] +fn init() { + logging::init_logging(); +} + #[tokio::test] async fn chain_genesis() { let url = "/api/v2/chain/genesis"; diff --git a/api-server/stack-test-suite/tests/v2/transaction.rs b/api-server/stack-test-suite/tests/v2/transaction.rs index a23cbbfc50..0bea9574f7 100644 --- a/api-server/stack-test-suite/tests/v2/transaction.rs +++ b/api-server/stack-test-suite/tests/v2/transaction.rs @@ -191,20 +191,20 @@ async fn multiple_tx_in_same_block(#[case] seed: Seed) { let transaction = signed_tx2.transaction(); let expected_transaction = json!({ - "block_id": block_id.to_hash().encode_hex::(), - "timestamp": block.timestamp().to_string(), - "confirmations": BlockHeight::new(0).to_string(), - "version_byte": transaction.version_byte(), - "is_replaceable": transaction.is_replaceable(), - "flags": transaction.flags(), - "inputs": transaction.inputs().iter().zip(utxos).map(|(inp, utxo)| json!({ - "input": tx_input_to_json(inp, &TokenDecimals::Single(None), &chain_config), - "utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &chain_config, &TokenDecimals::Single(None))), + "block_id": block_id.to_hash().encode_hex::(), + "timestamp": block.timestamp().to_string(), + "confirmations": BlockHeight::new(0).to_string(), + "version_byte": transaction.version_byte(), + "is_replaceable": transaction.is_replaceable(), + "flags": transaction.flags(), + "inputs": transaction.inputs().iter().zip(utxos).map(|(inp, utxo)| json!({ + "input": tx_input_to_json(inp, &TokenDecimals::Single(None), &chain_config), + "utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &chain_config, &TokenDecimals::Single(None))), })).collect::>(), - "outputs": transaction.outputs() - .iter() - .map(|out| txoutput_to_json(out, &chain_config, &TokenDecimals::Single(None))) - .collect::>(), + "outputs": transaction.outputs() + .iter() + .map(|out| txoutput_to_json(out, &chain_config, &TokenDecimals::Single(None))) + .collect::>(), }); _ = tx.send(( @@ -337,20 +337,20 @@ async fn ok(#[case] seed: Seed) { }); let expected_transaction = json!({ - "block_id": block_id.to_hash().encode_hex::(), - "timestamp": block.timestamp().to_string(), - "confirmations": BlockHeight::new((n_blocks - block_height) as u64).to_string(), - "version_byte": transaction.version_byte(), - "is_replaceable": transaction.is_replaceable(), - "flags": transaction.flags(), - "inputs": transaction.inputs().iter().zip(utxos).map(|(inp, utxo)| json!({ - "input": tx_input_to_json(inp, &TokenDecimals::Single(None), &chain_config), - "utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &chain_config, &TokenDecimals::Single(None))), + "block_id": block_id.to_hash().encode_hex::(), + "timestamp": block.timestamp().to_string(), + "confirmations": BlockHeight::new((n_blocks - block_height) as u64).to_string(), + "version_byte": transaction.version_byte(), + "is_replaceable": transaction.is_replaceable(), + "flags": transaction.flags(), + "inputs": transaction.inputs().iter().zip(utxos).map(|(inp, utxo)| json!({ + "input": tx_input_to_json(inp, &TokenDecimals::Single(None), &chain_config), + "utxo": utxo.as_ref().map(|txo| txoutput_to_json(txo, &chain_config, &TokenDecimals::Single(None))), })).collect::>(), - "outputs": transaction.outputs() - .iter() - .map(|out| txoutput_to_json(out, &chain_config, &TokenDecimals::Single(None))) - .collect::>(), + "outputs": transaction.outputs() + .iter() + .map(|out| txoutput_to_json(out, &chain_config, &TokenDecimals::Single(None))) + .collect::>(), }); _ = tx.send(( diff --git a/api-server/stack-test-suite/tests/v2/transactions.rs b/api-server/stack-test-suite/tests/v2/transactions.rs index 4bd4eeed6b..1214f66df7 100644 --- a/api-server/stack-test-suite/tests/v2/transactions.rs +++ b/api-server/stack-test-suite/tests/v2/transactions.rs @@ -13,11 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use api_server_common::storage::storage_api::{ - block_aux_data::BlockAuxData, TransactionInfo, TxAdditionalInfo, +use serde_json::Value; + +use api_server_common::{ + storage::storage_api::{block_aux_data::BlockAuxData, TransactionInfo, TxAdditionalInfo}, + utils::get_block_compact_target, }; use api_web_server::api::json_helpers::to_tx_json_with_block_info; -use serde_json::Value; use super::*; @@ -164,6 +166,7 @@ async fn ok(#[case] seed: Seed) { block_id.into(), BlockHeight::new((n_blocks - idx) as u64), block.timestamp(), + get_block_compact_target(&block), ), tx_global_index as u64, ) diff --git a/api-server/stack-test-suite/tests/v2/utils.rs b/api-server/stack-test-suite/tests/v2/utils.rs new file mode 100644 index 0000000000..b7b69f1efb --- /dev/null +++ b/api-server/stack-test-suite/tests/v2/utils.rs @@ -0,0 +1,76 @@ +// Copyright (c) 2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use chainstate_test_framework::TestFramework; +use common::{ + chain::{config::create_unit_test_config, Block, ChainConfig}, + primitives::{BlockHeight, Idable as _}, +}; +use crypto::{ + key::{KeyKind, PrivateKey}, + vrf::{VRFKeyKind, VRFPrivateKey}, +}; +use randomness::{CryptoRng, Rng}; + +/// Create a chain of the specified number of blocks, using either IgnoreConsensus or PoS. +pub fn create_chain( + block_count: usize, + use_pos: bool, + rng: &mut (impl Rng + CryptoRng), +) -> (ChainConfig, Vec) { + let (chain_config, tf) = if use_pos { + let (vrf_sk, vrf_pk) = VRFPrivateKey::new_from_rng(rng, VRFKeyKind::Schnorrkel); + let (staker_sk, staker_pk) = PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); + + let (chain_config_builder, genesis_pool_id) = + chainstate_test_framework::create_chain_config_with_default_staking_pool( + rng, staker_pk, vrf_pk, + ); + let chain_config = chain_config_builder.build(); + + let mut tf = TestFramework::builder(rng).with_chain_config(chain_config.clone()).build(); + + // Note: create_chain_pos_randomizing_time will advance time after creating each block, + // so we need to do the advancement explicitly before the first one. + let target_block_time = chain_config.target_block_spacing(); + let time_advancement = rng.gen_range(1..target_block_time.as_secs() * 2); + tf.progress_time_seconds_since_epoch(time_advancement); + + tf.create_chain_pos_randomizing_time( + rng, + &tf.genesis().get_id().into(), + block_count, + genesis_pool_id, + &staker_sk, + &vrf_sk, + ) + .unwrap(); + + (chain_config, tf) + } else { + let chain_config = create_unit_test_config(); + + let mut tf = TestFramework::builder(rng).with_chain_config(chain_config.clone()).build(); + + tf.create_chain_advancing_time_return_ids(&tf.genesis().get_id().into(), block_count, rng) + .unwrap(); + + (chain_config, tf) + }; + + let blocks = tf.chainstate.get_mainchain_blocks(BlockHeight::new(1), usize::MAX).unwrap(); + + (chain_config, blocks) +} diff --git a/api-server/storage-test-suite/src/basic.rs b/api-server/storage-test-suite/src/basic.rs index 7bfa1fac0b..a55162769b 100644 --- a/api-server/storage-test-suite/src/basic.rs +++ b/api-server/storage-test-suite/src/basic.rs @@ -21,15 +21,18 @@ use crate::helpers::make_trial; use crate::make_test; use pos_accounting::PoolData; -use api_server_common::storage::{ - impls::CURRENT_STORAGE_VERSION, - storage_api::{ - block_aux_data::{BlockAuxData, BlockWithExtraData}, - ApiServerStorage, ApiServerStorageError, ApiServerStorageRead, ApiServerStorageWrite, - ApiServerTransactionRw, BlockInfo, CoinOrTokenStatistic, Delegation, FungibleTokenData, - LockedUtxo, Order, PoolDataWithExtraInfo, TransactionInfo, Transactional, TxAdditionalInfo, - Utxo, UtxoLock, UtxoWithExtraInfo, +use api_server_common::{ + storage::{ + impls::CURRENT_STORAGE_VERSION, + storage_api::{ + block_aux_data::{BlockAuxData, BlockWithExtraData}, + ApiServerStorage, ApiServerStorageError, ApiServerStorageRead, ApiServerStorageWrite, + ApiServerTransactionRw, BlockInfo, CoinOrTokenStatistic, Delegation, FungibleTokenData, + LockedUtxo, Order, PoolDataWithExtraInfo, TransactionInfo, Transactional, + TxAdditionalInfo, Utxo, UtxoLock, UtxoWithExtraInfo, + }, }, + utils::get_block_compact_target, }; use crypto::{ key::{KeyKind, PrivateKey}, @@ -50,7 +53,9 @@ use common::{ AccountNonce, Block, DelegationId, Destination, OrderId, OutPointSourceId, PoolId, SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, }, - primitives::{per_thousand::PerThousand, Amount, BlockHeight, CoinOrTokenId, Id, Idable, H256}, + primitives::{ + per_thousand::PerThousand, Amount, BlockHeight, CoinOrTokenId, Compact, Id, Idable, H256, + }, }; use futures::Future; use libtest_mimic::Failed; @@ -136,7 +141,7 @@ where let genesis_id = chain_config.genesis_block_id(); let num_blocks = rng.gen_range(10..20); test_framework - .create_chain_return_ids_with_advancing_time(&genesis_id, num_blocks, &mut rng) + .create_chain_advancing_time_return_ids(&genesis_id, num_blocks, &mut rng) .unwrap(); let block_id1 = @@ -221,6 +226,7 @@ where block_id.into(), BlockHeight::new(block_height), block.timestamp(), + get_block_compact_target(&block), ), ) .await @@ -370,7 +376,7 @@ where let height1 = height1_u64.into(); let random_block_timestamp = BlockTimestamp::from_int_seconds(rng.gen::()); let aux_data1 = - BlockAuxData::new(owning_block1.into(), height1, random_block_timestamp); + BlockAuxData::new(owning_block1.into(), height1, random_block_timestamp, None); db_tx.set_block_aux_data(owning_block1, &aux_data1).await.unwrap(); let tx_info = TransactionInfo { @@ -421,8 +427,13 @@ where let existing_block_id: Id = block_id; let height1_u64 = rng.gen_range::(1..i64::MAX as u64); let height1 = height1_u64.into(); - let aux_data1 = - BlockAuxData::new(existing_block_id.into(), height1, random_block_timestamp); + let compact_target1 = Compact(rng.gen()); + let aux_data1 = BlockAuxData::new( + existing_block_id.into(), + height1, + random_block_timestamp, + Some(compact_target1), + ); db_tx.set_block_aux_data(existing_block_id, &aux_data1).await.unwrap(); let retrieved_aux_data = db_tx.get_block_aux_data(existing_block_id).await.unwrap(); @@ -432,8 +443,13 @@ where let height2_u64 = rng.gen_range::(1..i64::MAX as u64); let height2 = height2_u64.into(); let random_block_timestamp = BlockTimestamp::from_int_seconds(rng.gen::()); - let aux_data2 = - BlockAuxData::new(existing_block_id.into(), height2, random_block_timestamp); + let compact_target2 = Compact(rng.gen()); + let aux_data2 = BlockAuxData::new( + existing_block_id.into(), + height2, + random_block_timestamp, + Some(compact_target2), + ); db_tx.set_block_aux_data(existing_block_id, &aux_data2).await.unwrap(); let retrieved_aux_data = db_tx.get_block_aux_data(existing_block_id).await.unwrap(); diff --git a/api-server/web-server/src/api/json_helpers.rs b/api-server/web-server/src/api/json_helpers.rs index 0da984e7d8..2fa9117411 100644 --- a/api-server/web-server/src/api/json_helpers.rs +++ b/api-server/web-server/src/api/json_helpers.rs @@ -15,8 +15,11 @@ use std::{collections::BTreeMap, ops::Sub}; -use api_server_common::storage::storage_api::{ - block_aux_data::BlockAuxData, NftWithOwner, Order, TransactionInfo, TxAdditionalInfo, +use api_server_common::{ + storage::storage_api::{ + block_aux_data::BlockAuxData, NftWithOwner, Order, TransactionInfo, TxAdditionalInfo, + }, + utils::{unpack_block_compact_target, BlockCompactTargetUnpackingError}, }; use common::{ address::Address, @@ -610,3 +613,21 @@ pub fn pool_data_to_json( "delegations_balance": amount_to_json(pool_data.delegations_balance, chain_config.coin_decimals()), }) } + +// Json to return via endpoints such as "/chain" and "/chain/tip". +pub fn block_info_to_json( + aux_data: &BlockAuxData, +) -> Result { + let target = aux_data + .block_compact_target() + .map(unpack_block_compact_target) + .transpose()? + .map(|target| format!("0x{target:x}")); + + Ok(json!({ + "block_id": aux_data.block_id(), + "block_height": aux_data.block_height(), + "timestamp": aux_data.block_timestamp().as_int_seconds(), + "target": target + })) +} diff --git a/api-server/web-server/src/api/v2.rs b/api-server/web-server/src/api/v2.rs index 0cde5cc75c..1b2eddc55a 100644 --- a/api-server/web-server/src/api/v2.rs +++ b/api-server/web-server/src/api/v2.rs @@ -13,27 +13,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{ - api::json_helpers::{ - self, amount_to_json, block_header_to_json, pool_data_to_json, to_tx_json_with_block_info, - tx_to_json, txoutput_to_json, utxo_outpoint_to_json, TokenDecimals, - }, - error::{ - ApiServerWebServerClientError, ApiServerWebServerError, ApiServerWebServerForbiddenError, - ApiServerWebServerNotFoundError, ApiServerWebServerServerError, - }, - TxSubmitClient, -}; -use api_server_common::storage::storage_api::{ - block_aux_data::BlockAuxData, AmountWithDecimals, ApiServerStorage, ApiServerStorageRead, - BlockInfo, CoinOrTokenStatistic, Order, TransactionInfo, +use std::{ + collections::{BTreeMap, BTreeSet}, + ops::Sub, + str::FromStr, + sync::Arc, + time::Duration, }; + use axum::{ extract::{DefaultBodyLimit, Path, Query, State}, response::IntoResponse, routing::{get, post}, Json, Router, }; +use hex::ToHex; +use serde::Deserialize; +use serde_json::json; + +use api_server_common::storage::storage_api::{ + block_aux_data::BlockAuxData, AmountWithDecimals, ApiServerStorage, ApiServerStorageRead, + BlockInfo, CoinOrTokenStatistic, Order, TransactionInfo, +}; use common::{ address::Address, chain::{ @@ -43,20 +44,21 @@ use common::{ }, primitives::{Amount, BlockHeight, CoinOrTokenId, Id, Idable, H256}, }; -use hex::ToHex; -use serde::Deserialize; -use serde_json::json; use serialization::hex_encoded::HexEncoded; -use std::{ - collections::{BTreeMap, BTreeSet}, - ops::Sub, - str::FromStr, - sync::Arc, - time::Duration, -}; use utils::ensure; -use crate::ApiServerWebServerState; +use crate::{ + api::json_helpers::{ + self, amount_to_json, block_header_to_json, block_info_to_json, pool_data_to_json, + to_tx_json_with_block_info, tx_to_json, txoutput_to_json, utxo_outpoint_to_json, + TokenDecimals, + }, + error::{ + ApiServerWebServerClientError, ApiServerWebServerError, ApiServerWebServerForbiddenError, + ApiServerWebServerNotFoundError, ApiServerWebServerServerError, + }, + ApiServerWebServerState, TxSubmitClient, +}; use super::json_helpers::{nft_with_owner_to_json, to_json_string}; @@ -73,6 +75,7 @@ pub fn routes< let router = Router::new(); let router = router + .route("/chain", get(chain)) .route("/chain/genesis", get(chain_genesis)) .route("/chain/tip", get(chain_tip)) .route("/chain/:height", get(chain_at_height)); @@ -247,6 +250,67 @@ pub async fn block_transaction_ids( // chain/ // +pub async fn chain( + Query(params): Query>, + State(state): State, Arc>>, +) -> Result { + let offset_and_items = get_offset_and_items(¶ms)?; + + let db_tx = state.db.transaction_ro().await.map_err(|e| { + logging::log::error!("internal error: {e}"); + ApiServerWebServerError::ServerError(ApiServerWebServerServerError::InternalServerError) + })?; + + let blocks_aux_data = { + let mut blocks_count = offset_and_items.items; + let mut starting_height = offset_and_items.offset; + let need_genesis = starting_height == 0 && blocks_count > 0; + + if need_genesis { + starting_height += 1; + blocks_count -= 1; + } + + let mut blocks_aux_data = if blocks_count != 0 { + db_tx.get_blocks_aux_data(blocks_count, starting_height).await.map_err(|e| { + logging::log::error!("internal error: {e}"); + ApiServerWebServerError::ServerError( + ApiServerWebServerServerError::InternalServerError, + ) + })? + } else { + Vec::new() + }; + + if need_genesis { + let genesis = state.chain_config.genesis_block(); + + blocks_aux_data.insert( + 0, + BlockAuxData::new( + genesis.get_id().into(), + BlockHeight::zero(), + genesis.timestamp(), + None, + ), + ); + } + + blocks_aux_data + }; + + let blocks_aux_data = blocks_aux_data + .iter() + .map(block_info_to_json) + .collect::, _>>() + .map_err(|e| { + logging::log::error!("internal error: {e}"); + ApiServerWebServerError::ServerError(ApiServerWebServerServerError::InternalServerError) + })?; + + Ok(Json(serde_json::Value::Array(blocks_aux_data))) +} + #[allow(clippy::unused_async)] pub async fn chain_genesis( State(state): State, Arc>>, @@ -301,10 +365,12 @@ pub async fn chain_tip( ) -> Result { let best_block = best_block(&state).await?; - Ok(Json(json!({ - "block_height": best_block.block_height(), - "block_id": best_block.block_id().to_hash().encode_hex::(), - }))) + let json = block_info_to_json(&best_block).map_err(|e| { + logging::log::error!("internal error: {e}"); + ApiServerWebServerError::ServerError(ApiServerWebServerServerError::InternalServerError) + })?; + + Ok(Json(json)) } async fn best_block( @@ -1551,16 +1617,32 @@ struct OffsetAndItems { items: u32, } +pub const OFFSET_PARAM_NAME: &str = "offset"; +pub const ITEMS_PARAM_NAME: &str = "items"; +pub const DEFAULT_NUM_ITEMS: u32 = 10; +pub const MAX_NUM_ITEMS: u32 = 100; + fn get_offset_and_items( params: &BTreeMap, ) -> Result { - const OFFSET: &str = "offset"; - const ITEMS: &str = "items"; - const DEFAULT_NUM_ITEMS: u32 = 10; - const MAX_NUM_ITEMS: u32 = 100; + get_offset_and_items_generic( + params, + OFFSET_PARAM_NAME, + ITEMS_PARAM_NAME, + DEFAULT_NUM_ITEMS, + MAX_NUM_ITEMS, + ) +} +fn get_offset_and_items_generic( + params: &BTreeMap, + offset_param_name: &str, + items_param_name: &str, + default_num_items: u32, + max_num_items: u32, +) -> Result { let offset = params - .get(OFFSET) + .get(offset_param_name) .map(|offset| u64::from_str(offset)) .transpose() .map_err(|_| { @@ -1569,15 +1651,15 @@ fn get_offset_and_items( .unwrap_or_default(); let items = params - .get(ITEMS) + .get(items_param_name) .map(|items| u32::from_str(items)) .transpose() .map_err(|_| { ApiServerWebServerError::ClientError(ApiServerWebServerClientError::InvalidNumItems) })? - .unwrap_or(DEFAULT_NUM_ITEMS); + .unwrap_or(default_num_items); ensure!( - items <= MAX_NUM_ITEMS, + items <= max_num_items, ApiServerWebServerError::ClientError(ApiServerWebServerClientError::InvalidNumItems) ); diff --git a/chainstate/test-framework/Cargo.toml b/chainstate/test-framework/Cargo.toml index d560b9c7dc..ca45d463ca 100644 --- a/chainstate/test-framework/Cargo.toml +++ b/chainstate/test-framework/Cargo.toml @@ -15,6 +15,7 @@ common = { path = "../../common" } consensus = { path = "../../consensus" } constraints-value-accumulator = { path = "../constraints-value-accumulator" } crypto = { path = "../../crypto" } +logging = { path = "../../logging" } orders-accounting = { path = "../../orders-accounting" } pos-accounting = { path = "../../pos-accounting" } randomness = { path = "../../randomness" } diff --git a/chainstate/test-framework/src/framework.rs b/chainstate/test-framework/src/framework.rs index c30480c797..57b11b73c6 100644 --- a/chainstate/test-framework/src/framework.rs +++ b/chainstate/test-framework/src/framework.rs @@ -225,55 +225,52 @@ impl TestFramework { /// Create and process a given amount of blocks. Return the ids of the produced blocks. /// /// Each block contains a single transaction that spends a random amount from the previous - /// block outputs. + /// block outputs. The blocks will have identical timestamps. pub fn create_chain_return_ids( &mut self, parent_block: &Id, blocks_count: usize, rng: &mut (impl Rng + CryptoRng), ) -> Result>, ChainstateError> { - let mut prev_block_id = *parent_block; - let result = || -> Result>, ChainstateError> { - let mut ids = Vec::with_capacity(blocks_count); - for _ in 0..blocks_count { - let block = self - .make_block_builder() - .add_test_transaction_with_parent(prev_block_id, rng) - .with_parent(prev_block_id) - .build(&mut *rng); - prev_block_id = block.get_id().into(); - ids.push(prev_block_id); - self.do_process_block(block, BlockSource::Local)?; - } - - Ok(ids) - }(); - - self.refresh_block_indices()?; - result + self.create_chain_return_ids_impl(parent_block, blocks_count, false, rng) } /// Create and process a given amount of blocks. Return the ids of the produced blocks. /// /// Each block contains a single transaction that spends a random amount from the previous - /// block outputs. Each block has an incremented timestamp - pub fn create_chain_return_ids_with_advancing_time( + /// block outputs. The blocks will have increasing timestamps. + pub fn create_chain_advancing_time_return_ids( &mut self, parent_block: &Id, blocks_count: usize, rng: &mut (impl Rng + CryptoRng), ) -> Result>, ChainstateError> { + self.create_chain_return_ids_impl(parent_block, blocks_count, true, rng) + } + + fn create_chain_return_ids_impl( + &mut self, + parent_block: &Id, + blocks_count: usize, + advance_time: bool, + rng: &mut (impl Rng + CryptoRng), + ) -> Result>, ChainstateError> { + let target_block_time = self.chain_config().target_block_spacing(); + let mut prev_block_id = *parent_block; let result = || -> Result>, ChainstateError> { let mut ids = Vec::with_capacity(blocks_count); - let target_block_time = self.chain_config().target_block_spacing(); for _ in 0..blocks_count { - self.progress_time_seconds_since_epoch(target_block_time.as_secs()); + if advance_time { + let seconds = rng.gen_range(1..target_block_time.as_secs() * 2); + self.progress_time_seconds_since_epoch(seconds); + } + let block = self .make_block_builder() .add_test_transaction_with_parent(prev_block_id, rng) .with_parent(prev_block_id) - .build(rng); + .build(&mut *rng); prev_block_id = block.get_id().into(); ids.push(prev_block_id); self.do_process_block(block, BlockSource::Local)?; @@ -296,6 +293,12 @@ impl TestFramework { Ok(*self.create_chain_return_ids(parent_block, blocks_count, rng)?.last().unwrap()) } + /// Create the given number of blocks via PoS. + /// + /// After each block the current time will be advanced by a "target_block_spacing" seconds + /// exactly. + /// Note: if all blocks have "target_block_spacing" seconds between them, their targets + /// will be identical. pub fn create_chain_pos( &mut self, rng: &mut (impl Rng + CryptoRng), @@ -304,6 +307,52 @@ impl TestFramework { staking_pool: PoolId, staking_sk: &PrivateKey, staking_vrf_sk: &VRFPrivateKey, + ) -> Result, ChainstateError> { + self.create_chain_pos_impl( + rng, + parent_block, + blocks, + staking_pool, + staking_sk, + staking_vrf_sk, + false, + ) + } + + /// Create the given number of blocks via PoS. + /// + /// After each block the current time will be advanced by a random number of seconds + /// based on "target_block_spacing". + pub fn create_chain_pos_randomizing_time( + &mut self, + rng: &mut (impl Rng + CryptoRng), + parent_block: &Id, + blocks: usize, + staking_pool: PoolId, + staking_sk: &PrivateKey, + staking_vrf_sk: &VRFPrivateKey, + ) -> Result, ChainstateError> { + self.create_chain_pos_impl( + rng, + parent_block, + blocks, + staking_pool, + staking_sk, + staking_vrf_sk, + true, + ) + } + + #[allow(clippy::too_many_arguments)] + fn create_chain_pos_impl( + &mut self, + rng: &mut (impl Rng + CryptoRng), + parent_block: &Id, + blocks: usize, + staking_pool: PoolId, + staking_sk: &PrivateKey, + staking_vrf_sk: &VRFPrivateKey, + randomize_timediffs: bool, ) -> Result, ChainstateError> { let mut prev_block_id = *parent_block; let result = || -> Result, ChainstateError> { @@ -314,6 +363,7 @@ impl TestFramework { .with_stake_pool_id(staking_pool) .with_stake_spending_key(staking_sk.clone()) .with_vrf_key(staking_vrf_sk.clone()) + .with_randomized_timediffs(randomize_timediffs) .build(&mut *rng); prev_block_id = block.get_id().into(); self.do_process_block(block, BlockSource::Local)?; diff --git a/chainstate/test-framework/src/lib.rs b/chainstate/test-framework/src/lib.rs index bd27f7e45f..8f7cb15954 100644 --- a/chainstate/test-framework/src/lib.rs +++ b/chainstate/test-framework/src/lib.rs @@ -25,6 +25,7 @@ mod random_tx_maker; mod signature_destination_getter; mod staking_pools; pub mod storage; +mod test_block_index_handle; mod transaction_builder; mod tx_verification_strategy; mod utils; @@ -38,15 +39,17 @@ pub type TestChainstate = Box { tokens_accounting_store: InMemoryTokensAccounting, pos_accounting_store: InMemoryPoSAccounting, orders_accounting_store: InMemoryOrdersAccounting, + + randomize_timediffs: bool, } impl<'f> PoSBlockBuilder<'f> { @@ -130,6 +135,7 @@ impl<'f> PoSBlockBuilder<'f> { tokens_accounting_store, pos_accounting_store, orders_accounting_store, + randomize_timediffs: false, } } @@ -219,6 +225,11 @@ impl<'f> PoSBlockBuilder<'f> { .with_kernel_input(kernel_input_outpoint) } + pub fn with_randomized_timediffs(mut self, randomize: bool) -> Self { + self.randomize_timediffs = randomize; + self + } + fn build_impl(self, rng: &mut (impl Rng + CryptoRng)) -> (Block, &'f mut TestFramework) { let (consensus_data, block_timestamp) = match self.consensus_data { Some(data) => (data, self.timestamp), @@ -259,7 +270,12 @@ impl<'f> PoSBlockBuilder<'f> { }; let target_block_time = self.framework.chainstate.get_chain_config().target_block_spacing(); - self.framework.progress_time_seconds_since_epoch(target_block_time.as_secs()); + let time_advancement = if self.randomize_timediffs { + rng.gen_range(1..target_block_time.as_secs() * 2) + } else { + target_block_time.as_secs() + }; + self.framework.progress_time_seconds_since_epoch(time_advancement); let block = Block::new_from_header(signed_header, block_body).unwrap(); @@ -327,19 +343,13 @@ impl<'f> PoSBlockBuilder<'f> { ); let new_block_height = parent_block_index.block_height().next_height(); - let pos_status = match self - .framework - .chainstate - .get_chain_config() - .consensus_upgrades() - .consensus_status(new_block_height) - { - RequiredConsensus::PoS(status) => status, - RequiredConsensus::PoW(_) | RequiredConsensus::IgnoreConsensus => { - panic!("Invalid consensus") - } - }; - let current_difficulty = pos_status.get_chain_config().target_limit(); + let pos_status = get_pos_status(self.framework, new_block_height); + let target = calculate_new_pos_compact_target( + self.framework, + new_block_height, + &self.prev_block_hash, + ) + .unwrap(); let chain_config = self.framework.chainstate.get_chain_config().as_ref(); let epoch_index = chain_config.epoch_index_from_height(&new_block_height); @@ -359,7 +369,7 @@ impl<'f> PoSBlockBuilder<'f> { self.staking_pool.unwrap(), chain_config.final_supply().unwrap(), epoch_index, - current_difficulty.into(), + target, ) .unwrap() } diff --git a/chainstate/test-suite/src/tests/helpers/block_index_handle_impl.rs b/chainstate/test-framework/src/test_block_index_handle.rs similarity index 100% rename from chainstate/test-suite/src/tests/helpers/block_index_handle_impl.rs rename to chainstate/test-framework/src/test_block_index_handle.rs diff --git a/chainstate/test-framework/src/utils.rs b/chainstate/test-framework/src/utils.rs index 981688a508..22d6617e54 100644 --- a/chainstate/test-framework/src/utils.rs +++ b/chainstate/test-framework/src/utils.rs @@ -17,14 +17,17 @@ use std::borrow::Cow; use crate::{ framework::BlockOutputs, key_manager::KeyManager, - signature_destination_getter::SignatureDestinationGetter, TestFramework, + signature_destination_getter::SignatureDestinationGetter, TestBlockIndexHandle, TestFramework, }; use chainstate::{BlockIndex, GenBlockIndex}; -use chainstate_storage::BlockchainStorageRead; +use chainstate_storage::{BlockchainStorageRead, Transactional as _}; use chainstate_types::{pos_randomness::PoSRandomness, TipStorageTag}; use common::{ chain::{ - block::{consensus_data::PoSData, timestamp::BlockTimestamp, BlockRewardTransactable}, + block::{ + consensus_data::PoSData, timestamp::BlockTimestamp, BlockRewardTransactable, + ConsensusData, + }, config::{create_unit_test_config, Builder as ConfigBuilder, ChainType, EpochIndex}, output_value::OutputValue, signature::{ @@ -40,13 +43,13 @@ use common::{ }, stakelock::StakePoolData, Block, ChainConfig, CoinUnit, ConsensusUpgrade, Destination, GenBlock, Genesis, - NetUpgrades, OrderId, OutPointSourceId, PoSChainConfig, PoSChainConfigBuilder, PoolId, - TxInput, TxOutput, UtxoOutPoint, + NetUpgrades, OrderId, OutPointSourceId, PoSChainConfig, PoSChainConfigBuilder, PoSStatus, + PoolId, RequiredConsensus, TxInput, TxOutput, UtxoOutPoint, }, primitives::{per_thousand::PerThousand, Amount, BlockHeight, Compact, Id, Idable, H256}, Uint256, }; -use consensus::find_timestamp_for_staking; +use consensus::{find_timestamp_for_staking, ConsensusPoSError}; use crypto::{ key::{KeyKind, PrivateKey, PublicKey}, vrf::{VRFPrivateKey, VRFPublicKey}, @@ -556,3 +559,43 @@ where })) } } + +pub fn get_pos_status(tf: &TestFramework, block_height: BlockHeight) -> PoSStatus { + match tf + .chainstate + .get_chain_config() + .consensus_upgrades() + .consensus_status(block_height) + { + RequiredConsensus::PoS(status) => status, + RequiredConsensus::PoW(_) | RequiredConsensus::IgnoreConsensus => { + panic!("Invalid consensus") + } + } +} + +pub fn calculate_new_pos_compact_target( + tf: &TestFramework, + block_height: BlockHeight, + parent_block_id: &Id, +) -> Result { + let pos_status = get_pos_status(tf, block_height); + + let db_tx = tf.storage.transaction_ro().unwrap(); + let block_index_handle = + TestBlockIndexHandle::new(db_tx, tf.chainstate.get_chain_config().as_ref()); + + consensus::calculate_target_required( + tf.chainstate.get_chain_config().as_ref(), + &pos_status, + *parent_block_id, + &block_index_handle, + ) +} + +pub fn get_pos_target(block: &Block) -> Option { + match block.consensus_data() { + ConsensusData::None | ConsensusData::PoW(_) => None, + ConsensusData::PoS(data) => Some(data.compact_target().try_into().unwrap()), + } +} diff --git a/chainstate/test-suite/src/tests/helpers/mod.rs b/chainstate/test-suite/src/tests/helpers/mod.rs index 176214192e..5d2de2cc9f 100644 --- a/chainstate/test-suite/src/tests/helpers/mod.rs +++ b/chainstate/test-suite/src/tests/helpers/mod.rs @@ -26,7 +26,6 @@ use crypto::key::{KeyKind, PrivateKey}; use randomness::{CryptoRng, Rng}; pub mod block_creation_helpers; -pub mod block_index_handle_impl; pub mod block_status_helpers; pub mod in_memory_storage_wrapper; pub mod pos; diff --git a/chainstate/test-suite/src/tests/helpers/pos.rs b/chainstate/test-suite/src/tests/helpers/pos.rs index 92285d0e22..2e42c2632b 100644 --- a/chainstate/test-suite/src/tests/helpers/pos.rs +++ b/chainstate/test-suite/src/tests/helpers/pos.rs @@ -13,43 +13,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -use chainstate_storage::Transactional; -use chainstate_test_framework::TestFramework; +use chainstate_test_framework::{calculate_new_pos_compact_target, TestFramework}; use common::{ - chain::{CoinUnit, Genesis, RequiredConsensus}, + chain::{CoinUnit, Genesis}, primitives::{BlockHeight, Compact}, }; use consensus::ConsensusPoSError; use crypto::{key::PublicKey, vrf::VRFPublicKey}; -use super::block_index_handle_impl::TestBlockIndexHandle; - pub fn calculate_new_target( tf: &TestFramework, block_height: BlockHeight, ) -> Result { - let pos_status = match tf - .chainstate - .get_chain_config() - .consensus_upgrades() - .consensus_status(block_height) - { - RequiredConsensus::PoS(status) => status, - RequiredConsensus::PoW(_) | RequiredConsensus::IgnoreConsensus => { - panic!("Invalid consensus") - } - }; - - let db_tx = tf.storage.transaction_ro().unwrap(); - let block_index_handle = - TestBlockIndexHandle::new(db_tx, tf.chainstate.get_chain_config().as_ref()); - - consensus::calculate_target_required( - tf.chainstate.get_chain_config().as_ref(), - &pos_status, - tf.best_block_id(), - &block_index_handle, - ) + calculate_new_pos_compact_target(tf, block_height, &tf.best_block_id()) } pub fn create_custom_genesis_with_stake_pool( diff --git a/chainstate/test-suite/src/tests/history_iteration.rs b/chainstate/test-suite/src/tests/history_iteration.rs index a59137a8fc..30ae477421 100644 --- a/chainstate/test-suite/src/tests/history_iteration.rs +++ b/chainstate/test-suite/src/tests/history_iteration.rs @@ -13,14 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::helpers::block_index_handle_impl::TestBlockIndexHandle; +use rstest::rstest; use chainstate::BlockSource; use chainstate_storage::Transactional; -use chainstate_test_framework::TestFramework; +use chainstate_test_framework::{TestBlockIndexHandle, TestFramework}; use chainstate_types::BlockIndexHistoryIterator; use common::primitives::{Id, Idable, H256}; -use rstest::rstest; use test_utils::random::{make_seedable_rng, Seed}; #[rstest] diff --git a/common/src/primitives/compact.rs b/common/src/primitives/compact.rs index 4162912715..899ae3cee6 100644 --- a/common/src/primitives/compact.rs +++ b/common/src/primitives/compact.rs @@ -17,8 +17,10 @@ use crate::uint::Uint256; use serialization::{Decode, Encode}; use std::ops::Shl; +pub type InnerType = u32; + #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Encode, Decode, serde::Serialize)] -pub struct Compact(pub u32); +pub struct Compact(pub InnerType); impl std::fmt::Debug for Compact { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { From e93ba9350646d1d36c4b2bf8ead56739e852a152 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Fri, 5 Sep 2025 19:29:09 +0300 Subject: [PATCH 2/4] Scripts to plot block timestamp diffs and targets --- .dockerignore | 10 +- .gitignore | 6 +- build-tools/block-data-plots/README.md | 12 ++ .../api-server-docker-compose-mainnet/.env | 31 ++++ .../docker-compose.yml | 75 ++++++++ .../api-server-docker-compose-testnet/.env | 31 ++++ .../docker-compose.yml | 72 +++++++ build-tools/block-data-plots/collect_data.py | 114 ++++++++++++ build-tools/block-data-plots/show_plots.py | 175 ++++++++++++++++++ build-tools/docker/build.py | 13 +- .../docker/example-mainnet/docker-compose.yml | 2 + 11 files changed, 533 insertions(+), 8 deletions(-) create mode 100644 build-tools/block-data-plots/README.md create mode 100644 build-tools/block-data-plots/api-server-docker-compose-mainnet/.env create mode 100644 build-tools/block-data-plots/api-server-docker-compose-mainnet/docker-compose.yml create mode 100644 build-tools/block-data-plots/api-server-docker-compose-testnet/.env create mode 100644 build-tools/block-data-plots/api-server-docker-compose-testnet/docker-compose.yml create mode 100644 build-tools/block-data-plots/collect_data.py create mode 100644 build-tools/block-data-plots/show_plots.py diff --git a/.dockerignore b/.dockerignore index 9dca91015d..f8c2fb4e48 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,8 @@ .dockerignore .gitignore +build-tools/difficulty-plot/api-server-docker-compose + ###################################### # Below goes the contents of gitignore @@ -32,7 +34,10 @@ test/**/__pycache__ test/config.ini # The cache for docker container dependency -.cargo +.cargo/* + +# But do not ignore the cargo.toml file +!.cargo/config.toml # The cache for chain data in container .local @@ -52,3 +57,6 @@ wasm-wrappers/js-bindings-test/dist/ build-tools/docker/example-mainnet/mintlayer-data/* # Same for example-mainnet-dns-server. build-tools/docker/example-mainnet-dns-server/mintlayer-data/* + +# This directory will contain some generated files. +build-tools/block-data-plots/output diff --git a/.gitignore b/.gitignore index dbf53d624f..de3e36c93d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,9 +17,6 @@ #exclude python env env/ -#exclude env -.env - # Test Python cache test/**/__pycache__ @@ -50,3 +47,6 @@ wasm-wrappers/js-bindings-test/dist/ build-tools/docker/example-mainnet/mintlayer-data/* # Same for example-mainnet-dns-server. build-tools/docker/example-mainnet-dns-server/mintlayer-data/* + +# This directory will contain some generated files. +build-tools/block-data-plots/output diff --git a/build-tools/block-data-plots/README.md b/build-tools/block-data-plots/README.md new file mode 100644 index 0000000000..dd7d6bc20f --- /dev/null +++ b/build-tools/block-data-plots/README.md @@ -0,0 +1,12 @@ +## A bunch of helper scripts to produce a target and a timestamp difference plots. + +First run `collect_data.py` to collect data from an api server instance. + +Then run `show_plots.py` to show the plots. By default, the script will plot the entire set of +data; use the `--recent-days` parameter to only plot the data from the last few days. + +`api-server-docker-compose-mainnet` and `api-server-docker-compose-testnet` are helper +docker compose projects that spin up a node and an API server instance.\ +They're basically copies of `build-tools/docker/example-mainnet` with unneeded executables removed.\ +Use `build-tools/docker/build.py --latest` to build the images and tag them as `latest` (which is +expected by the projects' `.env` files). diff --git a/build-tools/block-data-plots/api-server-docker-compose-mainnet/.env b/build-tools/block-data-plots/api-server-docker-compose-mainnet/.env new file mode 100644 index 0000000000..94f9d48133 --- /dev/null +++ b/build-tools/block-data-plots/api-server-docker-compose-mainnet/.env @@ -0,0 +1,31 @@ +# This will be used as a prefix for container and volume names. +COMPOSE_PROJECT_NAME=mintlayer-block-data-plot-api-server-mainnet + +# Dockerhub username, from which the docker images will be pulled. +ML_DOCKERHUB_USERNAME=mintlayer +# The image tag to use. +ML_DOCKER_IMAGE_TAG=latest + +# The user and group ids that will be used to run the software. +ML_USER_ID=10001 +ML_GROUP_ID=10001 + +# User name and db name for the postgres dbms used by the api server. +# Note that the db will be created when the volume is first initialized, so changing +# the name later won't work. +API_SERVER_POSTGRES_USER=postgres +API_SERVER_POSTGRES_DB=postgres +# The password for the postgres dbms. +API_SERVER_POSTGRES_PASSWORD=password +# The docker image tag to use. +API_SERVER_POSTGRES_DOCKER_IMAGE_TAG=17.6 + +# Username and password for node rpc calls. +NODE_RPC_USERNAME=username +NODE_RPC_PASSWORD=password + +# Host machine's port to which api server's postgres port will be mapped. +API_SERVER_POSTGRES_HOST_PORT=5434 + +# The API web server's port will be mapped to this port on the host machine. +API_SERVER_HOST_PORT=3000 diff --git a/build-tools/block-data-plots/api-server-docker-compose-mainnet/docker-compose.yml b/build-tools/block-data-plots/api-server-docker-compose-mainnet/docker-compose.yml new file mode 100644 index 0000000000..815363b38f --- /dev/null +++ b/build-tools/block-data-plots/api-server-docker-compose-mainnet/docker-compose.yml @@ -0,0 +1,75 @@ +# Note: this is a simplified copy of `build-tools/docker/example-mainnet/docker-compose.yml`. +# Check that file for some additional comments about the configuration parameters being used. + +x-common-env: &ml-common-env + RUST_LOG: + ML_USER_ID: + ML_GROUP_ID: + +services: + node-daemon: + image: $ML_DOCKERHUB_USERNAME/node-daemon:$ML_DOCKER_IMAGE_TAG + command: node-daemon mainnet + environment: + <<: *ml-common-env + ML_MAINNET_NODE_RPC_BIND_ADDRESS: 0.0.0.0:3030 + ML_MAINNET_NODE_RPC_USERNAME: $NODE_RPC_USERNAME + ML_MAINNET_NODE_RPC_PASSWORD: $NODE_RPC_PASSWORD + volumes: + # Unlike `build-tools/docker/example-mainnet/docker-compose.yml`, this project stores + # the node data in a named volume instead of mounting it in the current directory. + - node_data:/home/mintlayer + + api-postgres-db: + image: postgres:$API_SERVER_POSTGRES_DOCKER_IMAGE_TAG + restart: always + environment: + POSTGRES_USER: $API_SERVER_POSTGRES_USER + POSTGRES_PASSWORD: $API_SERVER_POSTGRES_PASSWORD + POSTGRES_DB: $API_SERVER_POSTGRES_DB + ports: + - "127.0.0.1:$API_SERVER_POSTGRES_HOST_PORT:5432" + volumes: + - api_postgres_db:/var/lib/postgresql/data + + api-blockchain-scanner-daemon: + image: $ML_DOCKERHUB_USERNAME/api-blockchain-scanner-daemon:$ML_DOCKER_IMAGE_TAG + command: api-blockchain-scanner-daemon + depends_on: + - api-postgres-db + - node-daemon + environment: + <<: *ml-common-env + ML_API_SCANNER_DAEMON_NETWORK: mainnet + ML_API_SCANNER_DAEMON_POSTGRES_HOST: api-postgres-db + ML_API_SCANNER_DAEMON_POSTGRES_USER: $API_SERVER_POSTGRES_USER + ML_API_SCANNER_DAEMON_POSTGRES_PASSWORD: $API_SERVER_POSTGRES_PASSWORD + ML_API_SCANNER_DAEMON_POSTGRES_DATABASE: $API_SERVER_POSTGRES_DB + ML_API_SCANNER_DAEMON_NODE_RPC_ADDRESS: node-daemon:3030 + ML_API_SCANNER_DAEMON_NODE_RPC_USERNAME: $NODE_RPC_USERNAME + ML_API_SCANNER_DAEMON_NODE_RPC_PASSWORD: $NODE_RPC_PASSWORD + + api-web-server: + image: $ML_DOCKERHUB_USERNAME/api-web-server:$ML_DOCKER_IMAGE_TAG + command: api-web-server + depends_on: + - api-postgres-db + - api-blockchain-scanner-daemon + - node-daemon + environment: + <<: *ml-common-env + ML_API_WEB_SRV_NETWORK: mainnet + ML_API_WEB_SRV_BIND_ADDRESS: 0.0.0.0:3000 + ML_API_WEB_SRV_POSTGRES_HOST: api-postgres-db + ML_API_WEB_SRV_POSTGRES_USER: $API_SERVER_POSTGRES_USER + ML_API_WEB_SRV_POSTGRES_PASSWORD: $API_SERVER_POSTGRES_PASSWORD + ML_API_WEB_SRV_POSTGRES_DATABASE: $API_SERVER_POSTGRES_DB + ML_API_WEB_SRV_NODE_RPC_ADDRESS: node-daemon:3030 + ML_API_WEB_SRV_NODE_RPC_USERNAME: $NODE_RPC_USERNAME + ML_API_WEB_SRV_NODE_RPC_PASSWORD: $NODE_RPC_PASSWORD + ports: + - "$API_SERVER_HOST_PORT:3000" + +volumes: + api_postgres_db: + node_data: diff --git a/build-tools/block-data-plots/api-server-docker-compose-testnet/.env b/build-tools/block-data-plots/api-server-docker-compose-testnet/.env new file mode 100644 index 0000000000..de84eeb42c --- /dev/null +++ b/build-tools/block-data-plots/api-server-docker-compose-testnet/.env @@ -0,0 +1,31 @@ +# This will be used as a prefix for container and volume names. +COMPOSE_PROJECT_NAME=mintlayer-block-data-plot-api-server-testnet + +# Dockerhub username, from which the docker images will be pulled. +ML_DOCKERHUB_USERNAME=mintlayer +# The image tag to use. +ML_DOCKER_IMAGE_TAG=latest + +# The user and group ids that will be used to run the software. +ML_USER_ID=10001 +ML_GROUP_ID=10001 + +# User name and db name for the postgres dbms used by the api server. +# Note that the db will be created when the volume is first initialized, so changing +# the name later won't work. +API_SERVER_POSTGRES_USER=postgres +API_SERVER_POSTGRES_DB=postgres +# The password for the postgres dbms. +API_SERVER_POSTGRES_PASSWORD=password +# The docker image tag to use. +API_SERVER_POSTGRES_DOCKER_IMAGE_TAG=17.6 + +# Username and password for node rpc calls. +NODE_RPC_USERNAME=username +NODE_RPC_PASSWORD=password + +# Host machine's port to which api server's postgres port will be mapped. +API_SERVER_POSTGRES_HOST_PORT=15434 + +# The API web server's port will be mapped to this port on the host machine. +API_SERVER_HOST_PORT=13000 diff --git a/build-tools/block-data-plots/api-server-docker-compose-testnet/docker-compose.yml b/build-tools/block-data-plots/api-server-docker-compose-testnet/docker-compose.yml new file mode 100644 index 0000000000..ac7903d833 --- /dev/null +++ b/build-tools/block-data-plots/api-server-docker-compose-testnet/docker-compose.yml @@ -0,0 +1,72 @@ +# Same as api-server-docker-compose-mainnet, but for the testnet. + +x-common-env: &ml-common-env + RUST_LOG: + ML_USER_ID: + ML_GROUP_ID: + +services: + node-daemon: + image: $ML_DOCKERHUB_USERNAME/node-daemon:$ML_DOCKER_IMAGE_TAG + command: node-daemon testnet + environment: + <<: *ml-common-env + ML_TESTNET_NODE_RPC_BIND_ADDRESS: 0.0.0.0:13030 + ML_TESTNET_NODE_RPC_USERNAME: $NODE_RPC_USERNAME + ML_TESTNET_NODE_RPC_PASSWORD: $NODE_RPC_PASSWORD + volumes: + - node_data:/home/mintlayer + + api-postgres-db: + image: postgres:$API_SERVER_POSTGRES_DOCKER_IMAGE_TAG + restart: always + environment: + POSTGRES_USER: $API_SERVER_POSTGRES_USER + POSTGRES_PASSWORD: $API_SERVER_POSTGRES_PASSWORD + POSTGRES_DB: $API_SERVER_POSTGRES_DB + ports: + - "127.0.0.1:$API_SERVER_POSTGRES_HOST_PORT:5432" + volumes: + - api_postgres_db:/var/lib/postgresql/data + + api-blockchain-scanner-daemon: + image: $ML_DOCKERHUB_USERNAME/api-blockchain-scanner-daemon:$ML_DOCKER_IMAGE_TAG + command: api-blockchain-scanner-daemon + depends_on: + - api-postgres-db + - node-daemon + environment: + <<: *ml-common-env + ML_API_SCANNER_DAEMON_NETWORK: testnet + ML_API_SCANNER_DAEMON_POSTGRES_HOST: api-postgres-db + ML_API_SCANNER_DAEMON_POSTGRES_USER: $API_SERVER_POSTGRES_USER + ML_API_SCANNER_DAEMON_POSTGRES_PASSWORD: $API_SERVER_POSTGRES_PASSWORD + ML_API_SCANNER_DAEMON_POSTGRES_DATABASE: $API_SERVER_POSTGRES_DB + ML_API_SCANNER_DAEMON_NODE_RPC_ADDRESS: node-daemon:13030 + ML_API_SCANNER_DAEMON_NODE_RPC_USERNAME: $NODE_RPC_USERNAME + ML_API_SCANNER_DAEMON_NODE_RPC_PASSWORD: $NODE_RPC_PASSWORD + + api-web-server: + image: $ML_DOCKERHUB_USERNAME/api-web-server:$ML_DOCKER_IMAGE_TAG + command: api-web-server + depends_on: + - api-postgres-db + - api-blockchain-scanner-daemon + - node-daemon + environment: + <<: *ml-common-env + ML_API_WEB_SRV_NETWORK: testnet + ML_API_WEB_SRV_BIND_ADDRESS: 0.0.0.0:3000 + ML_API_WEB_SRV_POSTGRES_HOST: api-postgres-db + ML_API_WEB_SRV_POSTGRES_USER: $API_SERVER_POSTGRES_USER + ML_API_WEB_SRV_POSTGRES_PASSWORD: $API_SERVER_POSTGRES_PASSWORD + ML_API_WEB_SRV_POSTGRES_DATABASE: $API_SERVER_POSTGRES_DB + ML_API_WEB_SRV_NODE_RPC_ADDRESS: node-daemon:13030 + ML_API_WEB_SRV_NODE_RPC_USERNAME: $NODE_RPC_USERNAME + ML_API_WEB_SRV_NODE_RPC_PASSWORD: $NODE_RPC_PASSWORD + ports: + - "$API_SERVER_HOST_PORT:3000" + +volumes: + api_postgres_db: + node_data: diff --git a/build-tools/block-data-plots/collect_data.py b/build-tools/block-data-plots/collect_data.py new file mode 100644 index 0000000000..5a0e225230 --- /dev/null +++ b/build-tools/block-data-plots/collect_data.py @@ -0,0 +1,114 @@ +import argparse +import os +import pathlib +import requests +import sys +import time +from collections import namedtuple +from urllib.parse import urlparse + + +SCRIPT_DIR = pathlib.Path(__file__).resolve().parent +DEFAULT_OUTPUT_FILE = SCRIPT_DIR.joinpath("output", "mainnet_block_timestamps_targets.csv") + +ITEMS_PER_REQUEST = 100 + + +BlockInfoFoHeight = namedtuple( + "BlockInfoFoHeight", ["timestamp", "target"]) + +class Error(Exception): + pass + + +class Handler(): + def __init__(self, args): + self.api_server_url = args.api_server_url + self.session = requests.Session() + self.output_file = pathlib.Path(args.output_file).resolve() + + if len(urlparse(self.api_server_url).scheme) == 0: + raise Error("The provided URL must contain a scheme") + + def url(self, path): + return f"{self.api_server_url}/api/v2/{path}" + + def get(self, path, params): + response = self.session.get(self.url(path), params=params) + response.raise_for_status() + return response.json() + + def run(self): + genesis_info = self.get("chain/genesis", {}) + + block_infos_by_height = {} + starting_height = 1 + last_print_time_ns = time.time_ns() + while True: + block_infos = self.get( + "chain", {"offset": starting_height, "items": ITEMS_PER_REQUEST}) + + # Sanity check + assert(len(block_infos) == 0 or block_infos[0]["block_height"] == starting_height) + + for block_info in block_infos: + height = block_info["block_height"] + block_infos_by_height[height] = BlockInfoFoHeight( + timestamp=block_info["timestamp"], + target=int(block_info["target"], 16) + ) + + starting_height += len(block_infos) + if len(block_infos) < ITEMS_PER_REQUEST: + break + + # Print something every second, to cnfirm that the script makes some progress. + if time.time_ns() - last_print_time_ns >= 1000000000: + print(f"Retrieved {len(block_infos_by_height)} block infos") + last_print_time_ns = time.time_ns() + + # Add the genesis (the only reason to add it last is to make the printed + # "Retrieved x block infos" lines look nicer). + block_infos_by_height[0] = BlockInfoFoHeight( + # Note: ["timestamp"]["timestamp"] is not a typo - we indeed return it + # in this weird way. + timestamp=genesis_info["timestamp"]["timestamp"], + # Use a bogus target. + target=0 + ) + + print(f"Writing {len(block_infos_by_height)} block infos to {self.output_file}") + os.makedirs(self.output_file.parent, exist_ok=True) + + with open(self.output_file, "w") as output: + prev_height = -1 + for height in sorted(block_infos_by_height.keys()): + # Sanity check + if height != prev_height + 1: + raise Error( + f"Block heights are not consecutive: current height is {height}, prev height is {prev_height}" + ) + prev_height = height + + block_info = block_infos_by_height[height] + output.write(f"{block_info.timestamp}, {block_info.target}\n") + + +def main(): + try: + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('--api-server-url', + help='API server URL', required=True) + parser.add_argument('--output-file', + help='Output file', default=DEFAULT_OUTPUT_FILE) + args = parser.parse_args() + + Handler(args).run() + except Error as e: + print(f"Error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/build-tools/block-data-plots/show_plots.py b/build-tools/block-data-plots/show_plots.py new file mode 100644 index 0000000000..7fb833e2b3 --- /dev/null +++ b/build-tools/block-data-plots/show_plots.py @@ -0,0 +1,175 @@ +import argparse +import csv +import mplcursors +import pathlib +from matplotlib import pyplot + + +SCRIPT_DIR = pathlib.Path(__file__).resolve().parent +OUTPUT_DIR = SCRIPT_DIR.joinpath("output") +DEFAULT_INPUT_FILE = OUTPUT_DIR.joinpath("mainnet_block_timestamps_targets.csv") +AVERAGE_BLOCKS_PER_DAY = 720 + +CLICK_TO_SHOW_TOOLTIP_TEXT = "click to show a tooltip (can be laggy)" + + +class Handler(): + def __init__(self, args): + # Note: the first item will correspond to genesis, with the timestamp being real and + # the target bogus. + print(f"Reading input data from {args.input_file}") + all_timestamps, all_targets = zip(*load_timestamps_targets(args.input_file)) + + # Note: + # self.all_targets[N] corresponds to the block at height N+1. + # self.all_time_diffs[N] is the timestamp difference between blocks at heights N+1 and N. + self.all_targets = all_targets[1:] + self.all_time_diffs = [t2 - t1 for t1, t2 in zip(all_timestamps, all_timestamps[1:])] + + if args.recent_days is None: + self.skipped_blocks = 0 + self.block_range_help_str = "entire history" + else: + last_blocks_to_show = min(args.recent_days * AVERAGE_BLOCKS_PER_DAY, len(self.all_targets)) + self.skipped_blocks = len(self.all_targets) - last_blocks_to_show + self.block_range_help_str = f"last {args.recent_days} days" + + self.targets = self.all_targets[self.skipped_blocks:] + self.time_diffs = self.all_time_diffs[self.skipped_blocks:] + self.starting_height = self.skipped_blocks + 1 + + def run(self): + print("Creating plots") + self.plot_targets() + self.plot_time_diffs() + print("Plots created") + + pyplot.show() + + def plot_targets(self): + figure, axes = pyplot.subplots(layout="constrained") + figure.canvas.manager.set_window_title(f"Targets, {self.block_range_help_str}") + + plot = scatter_from( + axes, + self.targets, + self.starting_height, + s=1, + ) + + # Note: the initial targets were very big, so if the entire range is plotted, + # the later difficulties will be hard to distinguish, unless we set the limit + # for the y axis values. + # + # Note: normal_y_lim is close to what is used by default, it leaves some space at + # the top of the plot to make it look better. + normal_y_lim = float(max(self.targets)) * 1.05 + y_lim = min(float(self.all_targets[20000]), normal_y_lim) + axes.set_ylim(bottom=0, top=y_lim) + + # For some reason, zero values on the axes are shifted to the right/top by default. + # For the y axis we've fixed it already by specifying bottom=0 above, now do the same + # for the x axis. + axes.set_xlim(left=self.skipped_blocks) + + axes.set_xlabel("Block height") + axes.set_ylabel("Target") + axes.set_title(f"Target plot, {CLICK_TO_SHOW_TOOLTIP_TEXT}", fontdict={'fontweight': 'bold'}) + + # Show the height as an integer + Handler.set_tooltips(plot, "height = {x:.0f}\ntarget = {y:.2e}s") + + def plot_time_diffs(self): + figure, axes = pyplot.subplots(layout="constrained") + figure.canvas.manager.set_window_title(f"Timestamp differences, {self.block_range_help_str}") + + plot = scatter_from( + axes, + self.time_diffs, + self.starting_height, + s=9, + ) + + axes.set_xlabel("Block height") + axes.set_ylabel("Difference between timestamps of this and the previous block, in seconds") + axes.set_title(f"Timestamp diff plot, {CLICK_TO_SHOW_TOOLTIP_TEXT}", fontdict={'fontweight': 'bold'}) + + # Show the values as integers. + Handler.set_tooltips(plot, "height = {x:.0f}\ntime diff = {y:.0f}s") + + average_time_diff = sum(self.time_diffs)/len(self.time_diffs) + min_time_diff = min(self.time_diffs) + max_time_diff_idx = max_value_idx(self.time_diffs) + max_time_diff_height = self.starting_height + max_time_diff_idx + max_time_diff = self.time_diffs[max_time_diff_idx] + + top_left_text = f"Min diff: {min_time_diff}s" + + top_left_text += f"\nMax diff: {max_time_diff}s, block {max_time_diff_height}" + if self.skipped_blocks != 0: + all_time_max = max(self.all_time_diffs) + top_left_text += f" (all time max: {all_time_max}s)" + + top_left_text += f"\nAverage diff: {average_time_diff:.3f}s" + if self.skipped_blocks != 0: + all_time_average = sum(self.all_time_diffs)/len(self.all_time_diffs) + top_left_text += f" (all time average: {all_time_average:.3f}s)" + + axes.text(0.01, 0.99, top_left_text, transform=axes.transAxes, ha="left", va="top", fontsize=12) + + # Here text_fmt is a format string, it must refer to the values as 'x' and 'y'. + @staticmethod + def set_tooltips(plot, text_fmt): + cursor = mplcursors.cursor(plot, hover=False) + + def cursor_add_handler(sel): + x, y = sel.target + text = text_fmt.format(x=x, y=y) + sel.annotation.set_text(text) + # Align the contents to the left (note that "horizontalalignment" aka "ha" only + # works for the entire text box, so individual text lines won't be affected by it). + sel.annotation.set_multialignment("left") + # Make the background non-transparent (it's hard to see otherwise). + sel.annotation.get_bbox_patch().set(alpha=1.) + + cursor.connect("add", cursor_add_handler) + + +def scatter_from(axes, y_vals, x_start, **kwargs): + return axes.scatter( + range(x_start, x_start + len(y_vals)), + y_vals, + **kwargs + ) + + +def load_timestamps_targets(input_file): + with open(input_file, 'r') as file: + csv_reader = csv.reader(file) + + load_timestamps_targets = [] + for timestamp, target in csv_reader: + load_timestamps_targets.append((int(timestamp), int(target))) + return load_timestamps_targets + + +def max_value_idx(list): + return max(enumerate(list), key=lambda item: item[1])[0] + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('--input-file', + help='Input file, produced by collect_data.py', + default=DEFAULT_INPUT_FILE) + parser.add_argument('--recent-days', + type=int, + help='If specified, only plot the block data corresponding to this number of recent days (approximately)') + args = parser.parse_args() + + Handler(args).run() + + +if __name__ == "__main__": + main() diff --git a/build-tools/docker/build.py b/build-tools/docker/build.py index 04324cf3a6..527c7b5cae 100644 --- a/build-tools/docker/build.py +++ b/build-tools/docker/build.py @@ -1,7 +1,12 @@ -import toml +import argparse import os +import pathlib import subprocess -import argparse +import toml + + +ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent.parent +ROOT_CARGO_TOML = ROOT_DIR.joinpath("Cargo.toml") def get_cargo_version(cargo_toml_path): @@ -44,7 +49,7 @@ def build_docker_image(dockerfile_path, image_name, tags, num_jobs=None): try: # Run the command - subprocess.check_call(command, shell=True) + subprocess.check_call(command, shell=True, cwd=ROOT_DIR) print(f"Built {image_name} successfully (the tags are: {full_tags}).") except subprocess.CalledProcessError as error: print(f"Failed to build {image_name}: {error}") @@ -121,7 +126,7 @@ def main(): parser.add_argument('--local_tags', nargs='*', help='Additional tags to apply (these won\'t be pushed)', default=[]) args = parser.parse_args() - version = args.version if args.version else get_cargo_version("Cargo.toml") + version = args.version if args.version else get_cargo_version(ROOT_CARGO_TOML) # Note: the CI currently takes the version from the release tag, so it always starts with "v", # but the version from Cargo.toml doesn't have this prefix. version = version.removeprefix("v") diff --git a/build-tools/docker/example-mainnet/docker-compose.yml b/build-tools/docker/example-mainnet/docker-compose.yml index ccce4bc9e7..27571eb55b 100644 --- a/build-tools/docker/example-mainnet/docker-compose.yml +++ b/build-tools/docker/example-mainnet/docker-compose.yml @@ -65,6 +65,7 @@ services: - node-daemon environment: <<: *ml-common-env + ML_API_SCANNER_DAEMON_NETWORK: mainnet ML_API_SCANNER_DAEMON_POSTGRES_HOST: api-postgres-db ML_API_SCANNER_DAEMON_POSTGRES_USER: $API_SERVER_POSTGRES_USER ML_API_SCANNER_DAEMON_POSTGRES_PASSWORD: $API_SERVER_POSTGRES_PASSWORD @@ -83,6 +84,7 @@ services: - node-daemon environment: <<: *ml-common-env + ML_API_WEB_SRV_NETWORK: mainnet ML_API_WEB_SRV_BIND_ADDRESS: 0.0.0.0:3000 ML_API_WEB_SRV_POSTGRES_HOST: api-postgres-db ML_API_WEB_SRV_POSTGRES_USER: $API_SERVER_POSTGRES_USER From a0178f8ce3f3f8511dbca7c2699438db292ec121 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Fri, 5 Sep 2025 19:08:06 +0300 Subject: [PATCH 3/4] Update tracing-subscriber to address the vulnerability https://rustsec.org/advisories/RUSTSEC-2025-0055 --- Cargo.lock | 57 +++++++++++++----------------------------------------- 1 file changed, 13 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 171a2652ed..cc781670d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1071,7 +1071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata 0.4.9", + "regex-automata", "serde", ] @@ -4492,11 +4492,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -5043,16 +5043,6 @@ dependencies = [ "utils-networking", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -5619,12 +5609,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "owned_ttf_parser" version = "0.25.0" @@ -6226,7 +6210,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift 0.3.0", - "regex-syntax 0.8.5", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -6561,7 +6545,7 @@ dependencies = [ "crossterm", "fd-lock", "itertools 0.12.1", - "nu-ansi-term 0.50.1", + "nu-ansi-term", "serde", "strip-ansi-escapes", "strum", @@ -6599,17 +6583,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -6620,15 +6595,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -8675,14 +8644,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", - "nu-ansi-term 0.46.0", + "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", From 9cdb18493a5fef8cb2b7491eed88b5c9916e5d80 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Wed, 10 Sep 2025 22:27:11 +0300 Subject: [PATCH 4/4] API server: update all columns on conflict in set_mainchain_block in postgres storage; fix some inconsistencies between postgres and in-memory storages --- .../src/storage/impls/in_memory/mod.rs | 65 +++--- .../src/storage/impls/postgres/queries.rs | 10 +- api-server/storage-test-suite/src/basic.rs | 218 ++++++++++++++---- 3 files changed, 209 insertions(+), 84 deletions(-) diff --git a/api-server/api-server-common/src/storage/impls/in_memory/mod.rs b/api-server/api-server-common/src/storage/impls/in_memory/mod.rs index 5396efcfed..ccac831342 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/mod.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/mod.rs @@ -72,7 +72,7 @@ struct ApiServerInMemoryStorage { impl ApiServerInMemoryStorage { pub fn new(chain_config: &ChainConfig) -> Self { - let mut result = Self { + Self { block_table: BTreeMap::new(), block_aux_data_table: BTreeMap::new(), address_balance_table: BTreeMap::new(), @@ -99,12 +99,8 @@ impl ApiServerInMemoryStorage { None, ), number_of_coin_decimals: chain_config.coin_decimals(), - storage_version: super::CURRENT_STORAGE_VERSION, - }; - result - .initialize_storage(chain_config) - .expect("In-memory initialization must succeed"); - result + storage_version: CURRENT_STORAGE_VERSION, + } } fn is_initialized(&self) -> Result { @@ -819,35 +815,13 @@ impl ApiServerInMemoryStorage { } impl ApiServerInMemoryStorage { - fn initialize_storage( - &mut self, - _chain_config: &ChainConfig, - ) -> Result<(), ApiServerStorageError> { - self.storage_version = CURRENT_STORAGE_VERSION; - - Ok(()) - } - fn reinitialize_storage( &mut self, chain_config: &ChainConfig, ) -> Result<(), ApiServerStorageError> { - self.block_table.clear(); - self.block_aux_data_table.clear(); - self.address_balance_table.clear(); - self.address_locked_balance_table.clear(); - self.address_transactions_table.clear(); - self.delegation_table.clear(); - self.main_chain_blocks_table.clear(); - self.pool_data_table.clear(); - self.transaction_table.clear(); - self.utxo_table.clear(); - self.address_utxos.clear(); - self.fungible_token_data.clear(); - self.nft_token_issuances.clear(); - self.orders_table.clear(); - - self.initialize_storage(chain_config) + let mut new_storage = Self::new(chain_config); + std::mem::swap(self, &mut new_storage); + Ok(()) } fn del_address_balance_above_height( @@ -992,16 +966,37 @@ impl ApiServerInMemoryStorage { block_height: BlockHeight, block: &BlockWithExtraData, ) -> Result<(), ApiServerStorageError> { - let data = BlockAuxData::new( + let previously_stored_height = + self.block_aux_data_table.get(&block_id).map(|data| data.block_height()); + + let aux_data = BlockAuxData::new( block_id.into(), block_height, block.block.timestamp(), get_block_compact_target(&block.block), ); self.block_table.insert(block_id, block.clone()); - self.block_aux_data_table.insert(block_id, data); + self.block_aux_data_table.insert(block_id, aux_data); self.main_chain_blocks_table.insert(block_height, block_id); - self.best_block = data; + + // Handle a degenerate case when the block is stored several times using different heights + // (to be consistent with the postgres implementation). + if let Some(previously_stored_height) = previously_stored_height { + if previously_stored_height != block_height { + self.main_chain_blocks_table.remove(&previously_stored_height); + } + } + + if *self + .main_chain_blocks_table + .last_key_value() + .expect("the map is known to be non-empty") + .0 + == block_height + { + self.best_block = aux_data; + } + Ok(()) } diff --git a/api-server/api-server-common/src/storage/impls/postgres/queries.rs b/api-server/api-server-common/src/storage/impls/postgres/queries.rs index 0100240390..a37ebd85d6 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/queries.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/queries.rs @@ -1152,12 +1152,19 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { let timestamp = Self::block_time_to_postgres_friendly(block.block.timestamp())?; let compact_target = get_block_compact_target(&block.block).map(|target| target.0 as i64); + // Note: we need to update the block height on conflict, because the block may have been + // added as a stale one in the past, in which case its block_height hasn't been set. + // Regarding the rest of the columns, updating them on conflict makes no sense from the + // consensus point of view, because the data, if calculated correctly, can never change, + // provided that the block id stays the same. However, this is a low-level db call, + // so it'd be better if the caller decides what data can change and what can't. + // Also, this way it's consistent with the in-memory implementation. self.tx .execute( "INSERT INTO ml.blocks (block_id, block_height, block_timestamp, block_compact_target, block_data) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (block_id) DO UPDATE - SET block_data = $5, block_height = $2;", + SET block_height = $2, block_timestamp = $3, block_compact_target = $4, block_data = $5;", &[&block_id.encode(), &height, ×tamp, &compact_target, &block.encode()], ) .await @@ -2781,6 +2788,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { ) -> Result<(), ApiServerStorageError> { logging::log::debug!("Inserting block aux data with block_id {}", block_id); + // Note: we update the data on block id conflict, the reasons are the same as in set_mainchain_block. self.tx .execute( "INSERT INTO ml.block_aux_data (block_id, aux_data) VALUES ($1, $2) diff --git a/api-server/storage-test-suite/src/basic.rs b/api-server/storage-test-suite/src/basic.rs index a55162769b..91e553d667 100644 --- a/api-server/storage-test-suite/src/basic.rs +++ b/api-server/storage-test-suite/src/basic.rs @@ -97,7 +97,16 @@ where let mut storage = storage_maker().await; let mut tx = storage.transaction_rw().await.unwrap(); - let chain_config = create_unit_test_config(); + + // Note: we'll be creating a PoS chain so that PoS targets of blocks are not None. + let (vrf_sk, vrf_pk) = VRFPrivateKey::new_from_rng(&mut rng, VRFKeyKind::Schnorrkel); + let (staker_sk, staker_pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let (chain_config_builder, genesis_pool_id) = + chainstate_test_framework::create_chain_config_with_default_staking_pool( + &mut rng, staker_pk, vrf_pk, + ); + let chain_config = chain_config_builder.build(); + tx.reinitialize_storage(&chain_config).await.unwrap(); tx.commit().await.unwrap(); @@ -115,18 +124,20 @@ where // Test setting/getting blocks let block_id = { - let mut test_framework = TestFramework::builder(&mut rng).build(); - let chain_config = test_framework.chain_config().clone(); + let mut test_framework = + TestFramework::builder(&mut rng).with_chain_config(chain_config.clone()).build(); + let mut db_tx = storage.transaction_rw().await.unwrap(); - // should return genesis block id - let block_aux = db_tx.get_best_block().await.unwrap(); - assert_eq!(block_aux.block_height(), BlockHeight::new(0)); - assert_eq!(block_aux.block_id(), chain_config.genesis_block_id()); - assert_eq!( - block_aux.block_timestamp(), - chain_config.genesis_block().timestamp() + // should return genesis block data + let best_block_aux_data = db_tx.get_best_block().await.unwrap(); + let expected_best_block_aux_data = BlockAuxData::new( + chain_config.genesis_block_id(), + BlockHeight::new(0), + chain_config.genesis_block().timestamp(), + None, ); + assert_eq!(best_block_aux_data, expected_best_block_aux_data); let timestamps = db_tx.get_latest_blocktimestamps().await.unwrap(); assert_eq!(timestamps, vec![chain_config.genesis_block().timestamp()]); @@ -136,66 +147,179 @@ where let block = db_tx.get_block(random_block_id).await.unwrap(); assert!(block.is_none()); } - // Create a test framework and blocks + // Create test blocks let genesis_id = chain_config.genesis_block_id(); let num_blocks = rng.gen_range(10..20); + let target_block_time = chain_config.target_block_spacing(); + test_framework + .progress_time_seconds_since_epoch(rng.gen_range(1..target_block_time.as_secs() * 2)); test_framework - .create_chain_advancing_time_return_ids(&genesis_id, num_blocks, &mut rng) + .create_chain_pos_randomizing_time( + &mut rng, + &genesis_id, + num_blocks, + genesis_pool_id, + &staker_sk, + &vrf_sk, + ) .unwrap(); - let block_id1 = - test_framework.block_id(1).classify(&chain_config).chain_block_id().unwrap(); - let block1 = test_framework.block(block_id1); - let block_height = BlockHeight::new(1); - let block_info1 = BlockInfo { - block: BlockWithExtraData { - block: block1.clone(), - tx_additional_infos: vec![], - }, - height: Some(block_height), + let block1_height = BlockHeight::new(1); + let block1_id = test_framework + .block_id(block1_height.into_int()) + .classify(&chain_config) + .chain_block_id() + .unwrap(); + let block1 = test_framework.block(block1_id); + let block1_timestamp = block1.timestamp(); + let block1_compact_target = get_block_compact_target(&block1); + let block1_with_extras = BlockWithExtraData { + block: block1.clone(), + tx_additional_infos: vec![], }; { - let block_id = db_tx.get_block(block_id1).await.unwrap(); - assert!(block_id.is_none()); + let block1_aux_data = BlockAuxData::new( + block1_id.into(), + block1_height, + block1_timestamp, + block1_compact_target, + ); + + // Use a relatively big height distance so that the targets have the chance of being different. + let block2_height = BlockHeight::new(10); + let block2_id = test_framework + .block_id(block2_height.into_int()) + .classify(&chain_config) + .chain_block_id() + .unwrap(); + let block2 = test_framework.block(block2_id); + let block2_timestamp = block2.timestamp(); + let block2_compact_target = get_block_compact_target(&block2); + let block2_with_extras = BlockWithExtraData { + block: block2.clone(), + tx_additional_infos: vec![], + }; + + let block2_aux_data = BlockAuxData::new( + block2_id.into(), + block2_height, + block2_timestamp, + block2_compact_target, + ); - let block_id = db_tx.get_main_chain_block_id(block_height).await.unwrap(); + let block_info = db_tx.get_block(block1_id).await.unwrap(); + assert!(block_info.is_none()); + let block_info = db_tx.get_block(block2_id).await.unwrap(); + assert!(block_info.is_none()); + + let block_id = db_tx.get_main_chain_block_id(block1_height).await.unwrap(); + assert!(block_id.is_none()); + let block_id = db_tx.get_main_chain_block_id(block2_height).await.unwrap(); assert!(block_id.is_none()); - let block_with_extras = BlockWithExtraData { - block: block1.clone(), - tx_additional_infos: vec![], + db_tx + .set_mainchain_block(block1_id, block1_height, &block1_with_extras) + .await + .unwrap(); + + let block_info = db_tx.get_block(block1_id).await.unwrap(); + let expected_block_info = BlockInfo { + block: block1_with_extras.clone(), + height: Some(block1_height), }; + assert_eq!(block_info.unwrap(), expected_block_info); + + let block_id = db_tx.get_main_chain_block_id(block1_height).await.unwrap(); + assert_eq!(block_id.unwrap(), block1_id); + + // set_mainchain_block should have updated block's "aux data" too. + let aux_data = db_tx.get_blocks_aux_data(100, block1_height.into_int()).await.unwrap(); + assert_eq!(aux_data.len(), 1); + assert_eq!(aux_data[0], block1_aux_data); + + // The same is returned by get_best_block + let best_block_aux_data = db_tx.get_best_block().await.unwrap(); + assert_eq!(best_block_aux_data, block1_aux_data); + + // Call set_mainchain_block again using the same block id, but different data. + // The call should not try being smart and instead update the data as requested. db_tx - .set_mainchain_block(block_id1, block_height, &block_with_extras) + .set_mainchain_block(block1_id, block2_height, &block2_with_extras) .await .unwrap(); - let block = db_tx.get_block(block_id1).await.unwrap(); - assert_eq!(block.unwrap(), block_info1); + let block_info = db_tx.get_block(block1_id).await.unwrap(); + let expected_block_info = BlockInfo { + block: block2_with_extras.clone(), + height: Some(block2_height), + }; + assert_eq!(block_info.unwrap(), expected_block_info); + + // No main chain block on block1_height + let block_id = db_tx.get_main_chain_block_id(block1_height).await.unwrap(); + assert!(block_id.is_none()); + + // But there is one at block2_height, referring to block1_id. + let block_id = db_tx.get_main_chain_block_id(block2_height).await.unwrap(); + assert_eq!(block_id.unwrap(), block1_id); + + // The aux data should be updated as well + let aux_data = db_tx.get_blocks_aux_data(100, block1_height.into_int()).await.unwrap(); + let expected_aux_data2 = BlockAuxData::new( + block1_id.into(), + block2_height, + block2_timestamp, + block2_compact_target, + ); + assert_eq!(aux_data.len(), 1); + assert_eq!(aux_data[0], expected_aux_data2); - let block_id = db_tx.get_main_chain_block_id(block_height).await.unwrap(); - assert_eq!(block_id.unwrap(), block_id1); + // The same is returned by get_best_block + let best_block_aux_data = db_tx.get_best_block().await.unwrap(); + assert_eq!(best_block_aux_data, expected_aux_data2); // delete the main chain block db_tx - .del_main_chain_blocks_above_height(block_height.prev_height().unwrap()) + .del_main_chain_blocks_above_height(block2_height.prev_height().unwrap()) .await .unwrap(); // no main chain block on that height - let block_id = db_tx.get_main_chain_block_id(block_height).await.unwrap(); + let block_id = db_tx.get_main_chain_block_id(block2_height).await.unwrap(); assert!(block_id.is_none()); + // the mainchain aux data is no longer returned + let aux_data = db_tx.get_blocks_aux_data(100, block1_height.into_int()).await.unwrap(); + assert_eq!(aux_data.len(), 0); // but the block is still there just not on main chain - let block_info1 = BlockInfo { - block: BlockWithExtraData { - block: block1.clone(), - tx_additional_infos: vec![], - }, + let block_info = db_tx.get_block(block1_id).await.unwrap(); + let expected_block_info = BlockInfo { + block: block2_with_extras.clone(), height: None, }; - let block = db_tx.get_block(block_id1).await.unwrap(); - assert_eq!(block.unwrap(), block_info1); + assert_eq!(block_info.unwrap(), expected_block_info); + + // Set block1 and block2 as mainchain blocks, using the correct info, but in the + // reverse order - first block2, then block1. Check that block2 is the best block. + db_tx + .set_mainchain_block(block2_id, block2_height, &block2_with_extras) + .await + .unwrap(); + let best_block_aux_data = db_tx.get_best_block().await.unwrap(); + assert_eq!(best_block_aux_data, block2_aux_data); + + db_tx + .set_mainchain_block(block1_id, block1_height, &block1_with_extras) + .await + .unwrap(); + let best_block_aux_data = db_tx.get_best_block().await.unwrap(); + assert_eq!(best_block_aux_data, block2_aux_data); + + // Delete the mainchain blocks again. + db_tx + .del_main_chain_blocks_above_height(block1_height.prev_height().unwrap()) + .await + .unwrap(); } { @@ -261,7 +385,7 @@ where // delete the main chain block db_tx - .del_main_chain_blocks_above_height(block_height.prev_height().unwrap()) + .del_main_chain_blocks_above_height(block1_height.prev_height().unwrap()) .await .unwrap(); } @@ -271,7 +395,7 @@ where { // with read only tx reconfirm everything is the same after the commit let db_tx = storage.transaction_ro().await.unwrap(); - let block_id = db_tx.get_main_chain_block_id(block_height).await.unwrap(); + let block_id = db_tx.get_main_chain_block_id(block1_height).await.unwrap(); assert!(block_id.is_none()); let block_info1 = BlockInfo { @@ -281,11 +405,11 @@ where }, height: None, }; - let block = db_tx.get_block(block_id1).await.unwrap(); + let block = db_tx.get_block(block1_id).await.unwrap(); assert_eq!(block.unwrap(), block_info1); } - block_id1 + block1_id }; // Test setting/getting transactions @@ -461,8 +585,6 @@ where // Test setting/getting address spendable utxos { let db_tx = storage.transaction_ro().await.unwrap(); - let test_framework = TestFramework::builder(&mut rng).build(); - let chain_config = test_framework.chain_config().clone(); let (_bob_sk, bob_pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr);