// Copyright 2019 The Exonum Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // 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. //! Tests for the blockchain explorer functionality. use exonum::{ blockchain::{Schema, TransactionErrorType, TransactionSet, TxLocation}, crypto::{self, Hash}, explorer::*, helpers::Height, messages::{self, Message, RawTransaction, Signed}, node::ExternalMessage, }; use reqwest::Client; use crate::blockchain::{ create_block, create_blockchain, CreateWallet, ExplorerTransactions, Transfer, SERVICE_ID, }; use crate::node::run_node_with_message_len; #[test] #[allow(clippy::cognitive_complexity)] fn test_explorer_basics() { let mut blockchain = create_blockchain(); let (pk_alice, key_alice) = crypto::gen_keypair(); let (pk_bob, key_bob) = crypto::gen_keypair(); let payload_alice = CreateWallet::new(&pk_alice, "Alice"); let tx_alice = Message::sign_transaction(payload_alice.clone(), SERVICE_ID, pk_alice, &key_alice); let payload_bob = CreateWallet::new(&pk_bob, "Bob"); let tx_bob = Message::sign_transaction(payload_bob.clone(), SERVICE_ID, pk_bob, &key_bob); let payload_transfer = Transfer::new(&pk_alice, &pk_bob, 2); let tx_transfer = Message::sign_transaction(payload_transfer.clone(), SERVICE_ID, pk_alice, &key_alice); { let explorer = BlockchainExplorer::new(&blockchain); assert_eq!(explorer.height(), Height(0)); let block = explorer.block(Height(0)).unwrap(); assert_eq!(block.len(), 0); assert!(block.transaction(0).is_none()); assert!(explorer.transaction(&tx_alice.hash()).is_none()); } // Block #1: Alice's transaction. create_block(&mut blockchain, vec![tx_alice.clone()]); { let explorer = BlockchainExplorer::new(&blockchain); let snapshot = blockchain.snapshot(); let schema = Schema::new(&snapshot); assert_eq!(explorer.height(), Height(1)); assert_eq!(schema.transactions_len(), 1); assert!(explorer.block(Height(2)).is_none()); let block = explorer.block(Height(1)).unwrap(); assert_eq!(block.len(), 1); let tx_info = block.transaction(0).unwrap(); assert_eq!(*tx_info.location(), TxLocation::new(Height(1), 0)); assert_eq!(tx_info.status(), Ok(())); assert_eq!(tx_info.content().signed_message(), &tx_alice); assert_eq!( tx_info.content().signed_message().hash(), block.transaction_hashes()[0] ); let tx_info = explorer.transaction(&tx_alice.hash()).unwrap(); assert!(!tx_info.is_in_pool()); assert!(tx_info.is_committed()); assert_eq!(tx_info.content().signed_message(), &tx_alice); let tx_info = match tx_info { TransactionInfo::Committed(info) => info, tx => panic!("{:?}", tx), }; assert_eq!(*tx_info.location(), TxLocation::new(Height(1), 0)); assert_eq!( serde_json::to_value(&tx_info).unwrap(), json!({ "content": { "debug": payload_alice, "message": messages::to_hex_string(&tx_alice), "service_id": SERVICE_ID, }, "location": { "block_height": 1, "position_in_block": 0, }, "location_proof": tx_info.location_proof(), // too complicated to check "status": { "type": "success" }, "time": tx_info.time(), }) ); } // Block #2: other transactions. create_block(&mut blockchain, vec![tx_bob.clone(), tx_transfer.clone()]); let explorer = BlockchainExplorer::new(&blockchain); let snapshot = blockchain.snapshot(); let schema = Schema::new(&snapshot); assert_eq!(explorer.height(), Height(2)); assert_eq!(schema.transactions_len(), 3); let block = explorer.block(Height(2)).unwrap(); assert_eq!(block.len(), 2); let tx_info = block.transaction(0).unwrap(); let err = tx_info.status().unwrap_err(); assert_eq!(err.error_type(), TransactionErrorType::Code(1)); assert_eq!(err.description(), Some("Not allowed")); assert_eq!( serde_json::to_value(&tx_info).unwrap(), json!({ "content": { "debug": payload_bob, "message": messages::to_hex_string(&tx_bob), "service_id": SERVICE_ID, }, "location": { "block_height": 2, "position_in_block": 0, }, "location_proof": tx_info.location_proof(), // too complicated to check "status": { "type": "error", "code": 1, "description": "Not allowed", }, "time": tx_info.time(), }) ); let tx_info = block.transaction(1).unwrap(); let err = tx_info.status().unwrap_err(); assert_eq!(err.error_type(), TransactionErrorType::Panic); assert_eq!(err.description(), Some("oops")); assert_eq!( serde_json::to_value(&tx_info).unwrap(), json!({ "content": { "debug": payload_transfer, "message": messages::to_hex_string(&tx_transfer), "service_id": SERVICE_ID, }, "location": { "block_height": 2, "position_in_block": 1, }, "location_proof": tx_info.location_proof(), // too complicated to check "status": { "type": "panic", "description": "oops", }, "time": tx_info.time(), }) ); } #[test] fn test_explorer_pool_transaction() { let mut blockchain = create_blockchain(); let (pk_alice, key_alice) = crypto::gen_keypair(); let tx_alice = Message::sign_transaction( CreateWallet::new(&pk_alice, "Alice"), SERVICE_ID, pk_alice, &key_alice, ); let tx_hash = tx_alice.hash(); { let explorer = BlockchainExplorer::new(&blockchain); assert!(explorer.transaction(&tx_hash).is_none()); } let fork = blockchain.fork(); { let mut schema = Schema::new(&fork); schema.add_transaction_into_pool(tx_alice.clone()); } blockchain.merge(fork.into_patch()).unwrap(); let explorer = BlockchainExplorer::new(&blockchain); let tx_info = explorer.transaction(&tx_hash).unwrap(); assert!(tx_info.is_in_pool()); assert!(!tx_info.is_committed()); assert_eq!(tx_info.content().signed_message(), &tx_alice); } fn tx_generator() -> Box>> { Box::new((0..).map(|i| { let (pk, key) = crypto::gen_keypair(); Message::sign_transaction( CreateWallet::new(&pk, &format!("Alice #{}", i)), SERVICE_ID, pk, &key, ) })) } #[test] fn test_explorer_block_iter() { let mut blockchain = create_blockchain(); let mut tx_gen = tx_generator(); let txs = tx_gen.by_ref(); create_block(&mut blockchain, vec![]); // Height(1) create_block(&mut blockchain, txs.take(2).collect()); // Height(2) create_block(&mut blockchain, vec![]); // Height(3) create_block(&mut blockchain, txs.take(2).collect()); // Height(4) create_block(&mut blockchain, txs.take(3).collect()); // Height(5) create_block(&mut blockchain, vec![]); // Height(6) create_block(&mut blockchain, vec![]); // Height(7) create_block(&mut blockchain, vec![]); // Height(8) create_block(&mut blockchain, txs.take(5).collect()); // Height(9) assert_eq!(blockchain.last_block().height(), Height(9)); let explorer = BlockchainExplorer::new(&blockchain); let mut count = 0; for (i, block) in explorer.blocks(..).enumerate() { assert_eq!(block.height(), Height(i as u64)); count += 1; } assert_eq!(count, 10); let tx_count: usize = explorer.blocks(..Height(1_000)).map(|bl| bl.len()).sum(); assert_eq!(tx_count, 12); let count = explorer.blocks(..).filter(BlockInfo::is_empty).count(); assert_eq!(count, 6); let transaction_hashes: Vec = explorer .blocks(..) .flat_map(|info| info.transaction_hashes().to_vec()) .collect(); assert_eq!(transaction_hashes.len(), 12); let block = explorer.block(Height(4)).unwrap(); assert_eq!(transaction_hashes[3], block.transaction_hashes()[1]); let transactions: Vec = explorer .blocks(..) .flat_map(|info| info.with_transactions().transactions) .collect(); assert_eq!(transactions.len(), 12); assert!(transactions .iter() .all(|tx| tx.location().block_height() < Height(10))); let heights: Vec<_> = explorer .blocks(..) .rev() .filter(|bl| !bl.is_empty()) .map(|bl| bl.height()) .collect(); assert_eq!(heights, vec![Height(9), Height(5), Height(4), Height(2)]); let block = explorer .blocks(..) .filter(|bl| !bl.is_empty()) .nth(2) .unwrap(); assert_eq!(block.height(), Height(5)); let mut iter = explorer.blocks(..).filter(|bl| !bl.is_empty()).rev(); let _ = iter.by_ref().nth(1); let heights: Vec<_> = iter.by_ref().map(|bl| bl.height()).collect(); assert_eq!(heights, vec![Height(4), Height(2)]); assert!(iter.next().is_none()); let heights: Vec<_> = explorer .blocks(..Height(9)) .filter(|bl| !bl.is_empty()) .rev() .map(|bl| bl.height()) .collect(); assert_eq!(heights, vec![Height(5), Height(4), Height(2)]); let heights: Vec<_> = explorer .blocks(..Height(5)) .filter(|bl| !bl.is_empty()) .map(|bl| bl.height()) .collect(); assert_eq!(heights, vec![Height(2), Height(4)]); let heights: Vec<_> = explorer .blocks(Height(0)..Height(2)) .filter(|bl| !bl.is_empty()) .map(|bl| bl.height()) .collect(); assert_eq!(heights, vec![]); let heights: Vec<_> = explorer.blocks(..Height(2)).map(|bl| bl.height()).collect(); assert_eq!(heights, vec![Height(0), Height(1)]); let heights: Vec<_> = explorer.blocks(..Height(1)).map(|bl| bl.height()).collect(); assert_eq!(heights, vec![Height(0)]); let heights: Vec<_> = explorer.blocks(..Height(0)).map(|bl| bl.height()).collect(); assert_eq!(heights, vec![]); let heights: Vec<_> = explorer .blocks(Height(0)..Height(1)) .map(|bl| bl.height()) .collect(); assert_eq!(heights, vec![Height(0)]); let mut iter = explorer.blocks(..); assert_eq!(iter.by_ref().nth(3).unwrap().height(), Height(3)); assert_eq!(iter.nth(2).unwrap().height(), Height(6)); } #[test] fn test_transaction_iterator() { let mut blockchain = create_blockchain(); let txs = tx_generator(); create_block(&mut blockchain, txs.take(5).collect()); { let explorer = BlockchainExplorer::new(&blockchain); let block = explorer.block(Height(1)).unwrap(); for tx in &block { assert_eq!(tx.status(), Ok(())); } for (i, tx) in block.iter().enumerate() { let raw_tx = tx.content().raw_transaction(); let tx = ExplorerTransactions::tx_from_raw(raw_tx).unwrap(); match tx { ExplorerTransactions::CreateWallet(parsed_tx) => { assert_eq!(parsed_tx.name, format!("Alice #{}", i)) } _ => panic!("Transaction couldn't be parsed."), } } } // Test filtering and other nice stuff. let (pk_alice, key_alice) = crypto::gen_keypair(); let (pk_bob, key_bob) = crypto::gen_keypair(); let tx_alice = Message::sign_transaction( CreateWallet::new(&pk_alice, "Alice"), SERVICE_ID, pk_alice, &key_alice, ); let tx_bob = Message::sign_transaction( CreateWallet::new(&pk_bob, "Bob"), SERVICE_ID, pk_bob, &key_bob, ); let tx_transfer = Message::sign_transaction( Transfer::new(&pk_alice, &pk_bob, 2), SERVICE_ID, pk_alice, &key_alice, ); create_block( &mut blockchain, vec![tx_alice.clone(), tx_bob.clone(), tx_transfer.clone()], ); let explorer = BlockchainExplorer::new(&blockchain); let block = explorer.block(Height(2)).unwrap(); let failed_tx_hashes: Vec<_> = block .iter() .filter(|tx| tx.status().is_err()) .map(|tx| tx.content().signed_message().hash()) .collect(); assert_eq!(failed_tx_hashes, vec![tx_bob.hash(), tx_transfer.hash()]); let create_wallet_positions: Vec<_> = block .iter() .filter(|tx| { if let ExplorerTransactions::CreateWallet(_) = ExplorerTransactions::tx_from_raw(tx.content().raw_transaction()).unwrap() { true } else { false } }) .map(|tx| tx.location().position_in_block()) .collect(); assert_eq!(create_wallet_positions, vec![0, 1]); } #[test] fn test_block_with_transactions() { let mut blockchain = create_blockchain(); let txs: Vec<_> = tx_generator().take(5).collect(); create_block(&mut blockchain, txs); let explorer = BlockchainExplorer::new(&blockchain); let block = explorer.block_with_txs(Height(1)).unwrap(); assert_eq!(block.len(), 5); assert!(!block.is_empty()); assert!(block[1].status().is_ok()); let all_transactions_create_wallets = block.iter().all(|tx| { let tx = ExplorerTransactions::tx_from_raw(tx.content().raw_transaction()).unwrap(); if let ExplorerTransactions::CreateWallet(_) = tx { true } else { false } }); assert!(all_transactions_create_wallets); } #[test] #[should_panic(expected = "Index exceeds number of transactions")] fn test_block_with_transactions_index_overflow() { let mut blockchain = create_blockchain(); let txs: Vec<_> = tx_generator().take(5).collect(); create_block(&mut blockchain, txs); let explorer = BlockchainExplorer::new(&blockchain); let block = explorer.block_with_txs(Height(1)).unwrap(); assert!(block[6].status().is_ok()); } #[test] fn test_committed_transaction_roundtrip() { let mut blockchain = create_blockchain(); let tx = tx_generator().next().unwrap(); create_block(&mut blockchain, vec![tx.clone()]); let explorer = BlockchainExplorer::new(&blockchain); let tx_copy: &CommittedTransaction = &explorer.block_with_txs(Height(1)).unwrap()[0]; let json = serde_json::to_value(tx_copy).unwrap(); let tx_copy: CommittedTransaction = serde_json::from_value(json).unwrap(); assert_eq!(tx_copy.content().message(), &tx); } #[test] fn test_transaction_info_roundtrip() { let mut blockchain = create_blockchain(); let tx = tx_generator().next().unwrap(); let fork = blockchain.fork(); { let mut schema = Schema::new(&fork); schema.add_transaction_into_pool(tx.clone()); } blockchain.merge(fork.into_patch()).unwrap(); let explorer = BlockchainExplorer::new(&blockchain); let info: TransactionInfo = explorer.transaction(&tx.hash()).unwrap(); let json = serde_json::to_value(&info).unwrap(); let info: TransactionInfo = serde_json::from_value(json).unwrap(); assert_eq!(info.content().message(), &tx); } #[test] fn test_block_with_transactions_roundtrip() { let mut blockchain = create_blockchain(); let (pk_alice, key_alice) = crypto::gen_keypair(); let payload = CreateWallet::new(&pk_alice, "Alice"); let tx = Message::sign_transaction(payload.clone(), SERVICE_ID, pk_alice, &key_alice); create_block(&mut blockchain, vec![tx.clone()]); let explorer = BlockchainExplorer::new(&blockchain); let block = explorer.block_with_txs(Height(1)).unwrap(); let block_json = serde_json::to_value(&block).unwrap(); let block_copy: BlockWithTransactions = serde_json::from_value(block_json).unwrap(); assert_eq!( block_copy[0].content().message(), block[0].content().message() ); } #[test] fn test_sending_message_size() { let max_message_len = 512_usize; let node_handler = run_node_with_message_len(6339, 8088, max_message_len as u32); let client = Client::new(); let (pk, sk) = crypto::gen_keypair(); // Case when message len <= max_message_len let name = "a".repeat(371); // With name length 371 chars we'll get tx size: 512 bytes. let tx = Message::sign_transaction(CreateWallet::new(&pk, &name), SERVICE_ID, pk, &sk); assert_eq!(tx.signed_message().raw().len(), max_message_len); let tx_hash = tx.hash(); let tx_json = json!({ "tx_body": tx }); let mut result = client .post("http://localhost:8088/api/explorer/v1/transactions") .json(&tx_json) .send() .unwrap(); assert_eq!( serde_json::from_str::(&result.text().unwrap()).unwrap(), json!({ "tx_hash": tx_hash }) ); // Case when max_message_len < message len < max_payload_len let name = "a".repeat(372); let tx = Message::sign_transaction(CreateWallet::new(&pk, &name), SERVICE_ID, pk, &sk); assert_eq!(tx.signed_message().raw().len(), max_message_len + 1); let tx_json = json!({ "tx_body": tx }); let mut result = client .post("http://localhost:8088/api/explorer/v1/transactions") .json(&tx_json) .send() .unwrap(); assert_eq!( result.text().unwrap(), "Payload too large: the allowed message limit is 512 bytes, while received 513 bytes" ); // Case when message len >= max_payload_len let name = "a".repeat(436); let tx = Message::sign_transaction(CreateWallet::new(&pk, &name), SERVICE_ID, pk, &sk); assert_eq!(tx.signed_message().raw().len(), max_message_len + 65); let tx_json = json!({ "tx_body": tx }); let mut result = client .post("http://localhost:8088/api/explorer/v1/transactions") .json(&tx_json) .send() .unwrap(); assert_eq!( result.text().unwrap(), "Payload too large: the allowed json limit is 1088 bytes, while received 1168 bytes" ); node_handler .api_tx .send_external_message(ExternalMessage::Shutdown) .unwrap(); node_handler.node_thread.join().unwrap(); }