Skip to content

Commit

Permalink
Fix missing owning block for transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
azarovh committed Nov 8, 2024
1 parent afc10c0 commit b064992
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 152 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,15 @@ impl ApiServerInMemoryStorage {
owning_block: Option<Id<Block>>,
transaction: &TransactionInfo,
) -> Result<(), ApiServerStorageError> {
if let Some(owning_block) = owning_block {
// Emulate the behavior of real db where foreign key must be present
if !self.block_table.contains_key(&owning_block) {
return Err(ApiServerStorageError::LowLevelStorageError(
"Owning block must exist in block table".to_string(),
));
}
}

self.transaction_table
.insert(transaction_id, (owning_block, transaction.clone()));
Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use self::block_aux_data::{BlockAuxData, BlockWithExtraData};
pub mod block_aux_data;

#[allow(dead_code)]
#[derive(Debug, thiserror::Error)]
#[derive(Debug, Eq, PartialEq, thiserror::Error)]
pub enum ApiServerStorageError {
#[error("Low level storage error: {0}")]
LowLevelStorageError(String),
Expand Down
23 changes: 19 additions & 4 deletions api-server/scanner-lib/src/blockchain_state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,19 @@ impl<S: ApiServerStorage + Send + Sync> LocalBlockchainState for BlockchainState
logging::log::info!("Connected block: ({}, {:x})", block_height, block.get_id());

let mut total_fees = AccumulatedFee::new();
let mut transactions = Vec::with_capacity(block.transactions().len());
let mut tx_additional_infos = Vec::with_capacity(block.transactions().len());

// The order in which data is processed and flushed to the storage is very specific.
//
// First, the tx fee is calculated and the table dependant on tx info are updated. This is done
// per tx because calculating fee requires up to date utxo and orders info.
//
// Second, total fee is accumulated and block is flushed to the db.
//
// Third, txs are flushed to the db AFTER the block.
// This is done because transaction table has FOREIGN key `owning_block_id` referring block table.

for tx in block.transactions().iter() {
let (tx_fee, tx_additional_info) = calculate_tx_fee_and_collect_token_info(
&self.chain_config,
Expand All @@ -165,10 +176,7 @@ impl<S: ApiServerStorage + Send + Sync> LocalBlockchainState for BlockchainState
tx: tx.clone(),
additional_info: tx_additional_info,
};
db_tx
.set_transaction(tx.transaction().get_id(), Some(block.get_id()), &tx_info)
.await
.expect("Unable to set transaction");
transactions.push(tx_info);
}

let block_id = block.get_id();
Expand All @@ -181,6 +189,13 @@ impl<S: ApiServerStorage + Send + Sync> LocalBlockchainState for BlockchainState
.await
.expect("Unable to set block");

for tx_info in transactions {
db_tx
.set_transaction(tx_info.tx.transaction().get_id(), Some(block_id), &tx_info)
.await
.expect("Unable to set transaction");
}

db_tx
.set_block_aux_data(
block_id,
Expand Down
145 changes: 2 additions & 143 deletions api-server/stack-test-suite/tests/v2/transaction_merkle_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,9 @@
use super::*;

use api_server_common::storage::storage_api::{
block_aux_data::{BlockAuxData, BlockWithExtraData},
TransactionInfo, TxAdditionalInfo,
block_aux_data::BlockWithExtraData, TransactionInfo, TxAdditionalInfo,
};
use common::{
chain::{block::timestamp::BlockTimestamp, Block},
primitives::{Id, H256},
};
use std::str::FromStr;
use common::chain::Block;

#[tokio::test]
async fn get_transaction_failed() {
Expand All @@ -39,142 +34,6 @@ async fn get_transaction_failed() {
task.abort();
}

#[rstest]
#[trace]
#[case(Seed::from_entropy())]
#[tokio::test]
async fn get_block_failed(#[case] seed: Seed) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();

let (tx, rx) = tokio::sync::oneshot::channel();

let task = tokio::spawn(async move {
let web_server_state = {
let mut rng = make_seedable_rng(seed);
let block_height = rng.gen_range(1..50);
let n_blocks = rng.gen_range(block_height..100);

let chain_config = create_unit_test_config();

let (chainstate_blocks, signed_transaction, transaction_id) = {
let mut tf = TestFramework::builder(&mut rng)
.with_chain_config(chain_config.clone())
.build();

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 block_id = chainstate_block_ids[block_height - 1];
let block = tf.block(tf.to_chain_block_id(&block_id));

let transaction_index = rng.gen_range(0..block.transactions().len());
let signed_transaction = block.transactions()[transaction_index].clone();
let transaction = signed_transaction.transaction();
let transaction_id = transaction.get_id();

_ = tx.send(transaction_id.to_hash().encode_hex::<String>());

(
chainstate_block_ids
.iter()
.map(|id| tf.block(tf.to_chain_block_id(id)))
.collect::<Vec<_>>(),
signed_transaction,
transaction_id,
)
};

let mut 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();

storage = {
let mut storage = local_node.storage().clone_storage().await;
let mut db_tx = storage.transaction_rw().await.unwrap();

let block_id: Id<Block> = H256::from_str(
"0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap()
.into();

let tx_info = TransactionInfo {
tx: signed_transaction,
additional_info: TxAdditionalInfo {
fee: Amount::from_atoms(rng.gen_range(0..100)),
input_utxos: vec![],
token_decimals: BTreeMap::new(),
},
};

db_tx.set_transaction(transaction_id, Some(block_id), &tx_info).await.unwrap();

db_tx
.set_block_aux_data(
block_id,
&BlockAuxData::new(
block_id.into(),
BlockHeight::new(rng.gen::<u32>() as u64),
BlockTimestamp::from_int_seconds(rng.gen::<u64>()),
),
)
.await
.unwrap();

db_tx.commit().await.unwrap();

storage
};

ApiServerWebServerState {
db: Arc::new(storage),
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 transaction_id = rx.await.unwrap();
let url = format!("/api/v2/transaction/{transaction_id}/merkle-path");

// Given that the listener port is open, this will block until a
// response is made (by the web server, which takes the listener
// over)
let response = reqwest::get(format!("http://{}:{}{url}", addr.ip(), addr.port()))
.await
.unwrap();

assert_eq!(response.status(), 404);

let body = response.text().await.unwrap();
let body: serde_json::Value = serde_json::from_str(&body).unwrap();
let body = body.as_object().unwrap();

assert_eq!(body["error"].as_str().unwrap(), "Block not found");

task.abort();
}

#[rstest]
#[trace]
#[case(Seed::from_entropy())]
Expand Down
36 changes: 32 additions & 4 deletions api-server/storage-test-suite/src/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ use api_server_common::storage::{
impls::CURRENT_STORAGE_VERSION,
storage_api::{
block_aux_data::{BlockAuxData, BlockWithExtraData},
ApiServerStorage, ApiServerStorageRead, ApiServerStorageWrite, ApiServerTransactionRw,
BlockInfo, CoinOrTokenStatistic, Delegation, FungibleTokenData, LockedUtxo, Order,
TransactionInfo, TxAdditionalInfo, Utxo, UtxoLock, UtxoWithExtraInfo,
ApiServerStorage, ApiServerStorageError, ApiServerStorageRead, ApiServerStorageWrite,
ApiServerTransactionRw, BlockInfo, CoinOrTokenStatistic, Delegation, FungibleTokenData,
LockedUtxo, Order, TransactionInfo, TxAdditionalInfo, Utxo, UtxoLock, UtxoWithExtraInfo,
},
};
use crypto::{
Expand All @@ -52,7 +52,10 @@ use common::{
};
use futures::Future;
use libtest_mimic::Failed;
use test_utils::random::{make_seedable_rng, Seed};
use test_utils::{
assert_matches,
random::{make_seedable_rng, Seed},
};

pub async fn initialization<S, Fut, F: Fn() -> Fut>(
storage_maker: Arc<F>,
Expand Down Expand Up @@ -308,6 +311,31 @@ where

drop(db_tx);

// Set with not existing owning block
{
let mut db_tx = storage.transaction_rw().await.unwrap();

let tx_info = TransactionInfo {
tx: tx1.clone(),
additional_info: TxAdditionalInfo {
fee: Amount::from_atoms(rng.gen_range(0..100)),
input_utxos: tx1_input_utxos.clone(),
token_decimals: BTreeMap::new(),
},
};
let random_owning_block = Id::<Block>::new(H256::random_using(&mut rng));
let result = db_tx
.set_transaction(
tx1.transaction().get_id(),
Some(random_owning_block),
&tx_info,
)
.await
.unwrap_err();

assert_matches!(result, ApiServerStorageError::LowLevelStorageError(..));
}

let mut db_tx = storage.transaction_rw().await.unwrap();

// Set without owning block
Expand Down

0 comments on commit b064992

Please sign in to comment.