// Copyright (c) 2014-2022, The Monero Project // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are // permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this list of // conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, this list // of conditions and the following disclaimer in the documentation and/or other // materials provided with the distribution. // // 3. Neither the name of the copyright holder nor the names of its contributors may be // used to endorse or promote products derived from this software without specific // prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL // THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include #include #include #include #include "gtest/gtest.h" #include "string_tools.h" #include "blockchain_db/blockchain_db.h" #include "blockchain_db/asset_db.h" #include "blockchain_db/lmdb/db_lmdb.h" #include "cryptonote_basic/asset_confidential.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "device/device.hpp" #include "ringct/bulletproofs_plus.h" #include "ringct/rctSigs.h" using namespace cryptonote; using epee::string_tools::pod_to_hex; #define ASSERT_HASH_EQ(a,b) ASSERT_EQ(pod_to_hex(a), pod_to_hex(b)) namespace { // anonymous namespace assets::transaction_extension make_asset_extension(const crypto::hash& carrier) { crypto::public_key public_key{}; crypto::secret_key secret_key{}; crypto::generate_keys(public_key, secret_key); assets::transaction_extension extension; extension.network = TESTNET; extension.carrier_prefix_hash = carrier; extension.issuance.descriptor.network = TESTNET; extension.issuance.descriptor.issuer_key = public_key; extension.issuance.descriptor.atomic_supply = 1000000; extension.issuance.descriptor.display_decimals = 2; extension.issuance.descriptor.metadata_reference = "ipfs://monzero-db-test"; extension.issuance.descriptor.issuance_nonce.data[0] = carrier.data[0]; crypto::hash message{}; if (!assets::derive_issuance_authorization_hash(extension.issuance.descriptor, message)) throw std::runtime_error("failed to derive issuance authorization"); crypto::generate_signature(message, public_key, secret_key, extension.issuance.issuer_signature); return extension; } assets::asset_ownership_proof make_db_ownership_proof( const crypto::hash& id, const crypto::hash& carrier) { constexpr size_t real = 5; assets::asset_ownership_proof proof; proof.asset_id = id; rct::ctkeyV public_ring; rct::key spend_secret{}, input_mask{}; const rct::key amount = rct::d2h(10); for (size_t index = 0; index < assets::CONFIDENTIAL_ASSET_RING_SIZE; ++index) { assets::asset_ring_member member; member.asset_id = id; member.output_id.data[0] = static_cast(index + 1); rct::key ignored; rct::skpkGen(ignored, member.public_output.dest); rct::skpkGen(ignored, member.public_output.mask); proof.ring.push_back(member); } rct::skpkGen(spend_secret, proof.ring[real].public_output.dest); input_mask = rct::skGen(); rct::addKeys2(proof.ring[real].public_output.mask, input_mask, amount, rct::H); for (const auto& member : proof.ring) public_ring.push_back(member.public_output); const rct::key pseudo_mask = rct::skGen(); rct::addKeys2(proof.pseudo_input, pseudo_mask, amount, rct::H); rct::key message; std::string error; if (!assets::derive_asset_ownership_message(proof, TESTNET, carrier, message, &error)) throw std::runtime_error(error); rct::ctkey input_secret; input_secret.dest = spend_secret; input_secret.mask = input_mask; proof.signature = rct::proveRctCLSAGSimple(message, public_ring, input_secret, pseudo_mask, proof.pseudo_input, real, hw::get_device("default")); std::memcpy(&proof.key_image, &proof.signature.I, sizeof(proof.key_image)); return proof; } assets::asset_transaction_payload make_db_asset_issuance( const crypto::hash& carrier) { const assets::transaction_extension extension = make_asset_extension(carrier); assets::asset_transaction_payload payload; payload.network = TESTNET; payload.carrier_prefix_hash = carrier; payload.issuance = extension.issuance; crypto::hash id{}; if (!assets::derive_asset_id(extension.issuance.descriptor, id)) throw std::runtime_error("failed to derive test asset id"); const uint64_t supply = extension.issuance.descriptor.atomic_supply; assets::confidential_asset_balance balance; balance.asset_id = id; balance.pseudo_inputs.push_back({id, rct::commit(supply, rct::zero())}); balance.outputs.push_back(rct::commit(supply, rct::zero())); balance.range_proofs.push_back(rct::bulletproof_plus_PROVE(supply, rct::zero())); payload.balances.push_back(balance); rct::key secret{}, destination{}; rct::skpkGen(secret, destination); payload.output_destinations.push_back({destination}); return payload; } const std::vector t_blocks = { "0100d5adc49a053b8818b2b6023cd2d532c6774e164a8fcacd603651cb3ea0cb7f9340b28ec016b4bc4ca301aa0101ff6e08acbb2702eab03067870349139bee7eab2ca2e030a6bb73d4f68ab6a3b6ca937214054cdac0843d028bbe23b57ea9bae53f12da93bb57bf8a2e40598d9fccd10c2921576e987d93cd80b4891302468738e391f07c4f2b356f7957160968e0bfef6e907c3cee2d8c23cbf04b089680c6868f01025a0f41f063e195a966051e3a29e17130a9ce97d48f55285b9bb04bdd55a09ae78088aca3cf0202d0f26169290450fe17e08974789c3458910b4db18361cdc564f8f2d0bdd2cf568090cad2c60e02d6f3483ec45505cc3be841046c7a12bf953ac973939bc7b727e54258e1881d4d80e08d84ddcb0102dae6dfb16d3e28aaaf43e00170b90606b36f35f38f8a3dceb5ee18199dd8f17c80c0caf384a30202385d7e57a4daba4cdd9e550a92dcc188838386e7581f13f09de796cbed4716a42101c052492a077abf41996b50c1b2e67fd7288bcd8c55cdc657b4e22d0804371f6901beb76a82ea17400cd6d7f595f70e1667d2018ed8f5a78d1ce07484222618c3cd" , "0100f9adc49a057d3113f562eac36f14afa08c22ae20bbbf8cffa31a4466d24850732cb96f80e9762365ee01ab0101ff6f08cc953502be76deb845c431f2ed9a4862457654b914003693b8cd672abc935f0d97b16380c08db7010291819f2873e3efbae65ecd5a736f5e8a26318b591c21e39a03fb536520ac63ba80dac40902439a10fde02e39e48e0b31e57cc084a07eedbefb8cbea0143aedd0442b189caa80c6868f010227b84449de4cd7a48cbdce8974baf0b6646e03384e32055e705c243a86bef8a58088aca3cf0202fa7bd15e4e7e884307ab130bb9d50e33c5fcea6546042a26f948efd5952459ee8090cad2c60e028695583dbb8f8faab87e3ef3f88fa827db097bbf51761d91924f5c5b74c6631780e08d84ddcb010279d2f247b54690e3b491e488acff16014a825fd740c23988a25df7c4670c1f2580c0caf384a302022599dfa3f8788b66295051d85937816e1c320cdb347a0fba5219e3fe60c83b2421010576509c5672025d28fd5d3f38efce24e1f9aaf65dd3056b2504e6e2b7f19f7800" }; const std::vector t_sizes = { 1122 , 347 }; const std::vector t_diffs = { 4003674 , 4051757 }; const std::vector t_coins = { 1952630229575370 , 1970220553446486 }; const std::vector> t_transactions = { { "0100010280e08d84ddcb0106010401110701f254220bb50d901a5523eaed438af5d43f8c6d0e54ba0632eb539884f6b7c02008c0a8a50402f9c7cf807ae74e56f4ec84db2bd93cfb02c2249b38e306f5b54b6e05d00d543b8095f52a02b6abb84e00f47f0a72e37b6b29392d906a38468404c57db3dbc5e8dd306a27a880d293ad0302cfc40a86723e7d459e90e45d47818dc0e81a1f451ace5137a4af8110a89a35ea80b4c4c321026b19c796338607d5a2c1ba240a167134142d72d1640ef07902da64fed0b10cfc8088aca3cf02021f6f655254fee84161118b32e7b6f8c31de5eb88aa00c29a8f57c0d1f95a24dd80d0b8e1981a023321af593163cea2ae37168ab926efd87f195756e3b723e886bdb7e618f751c480a094a58d1d0295ed2b08d1cf44482ae0060a5dcc4b7d810a85dea8c62e274f73862f3d59f8ed80a0e5b9c2910102dc50f2f28d7ceecd9a1147f7106c8d5b4e08b2ec77150f52dd7130ee4f5f50d42101d34f90ac861d0ee9fe3891656a234ea86a8a93bf51a237db65baa00d3f4aa196a9e1d89bc06b40e94ea9a26059efc7ba5b2de7ef7c139831ca62f3fe0bb252008f8c7ee810d3e1e06313edf2db362fc39431755779466b635f12f9f32e44470a3e85e08a28fcd90633efc94aa4ae39153dfaf661089d045521343a3d63e8da08d7916753c66aaebd4eefcfe8e58e5b3d266b752c9ca110749fa33fce7c44270386fcf2bed4f03dd5dadb2dc1fd4c505419f8217b9eaec07521f0d8963e104603c926745039cf38d31de6ed95ace8e8a451f5a36f818c151f517546d55ac0f500e54d07b30ea7452f2e93fa4f60bdb30d71a0a97f97eb121e662006780fbf69002228224a96bff37893d47ec3707b17383906c0cd7d9e7412b3e6c8ccf1419b093c06c26f96e3453b424713cdc5c9575f81cda4e157052df11f4c40809edf420f88a3dd1f7909bbf77c8b184a933389094a88e480e900bcdbf6d1824742ee520fc0032e7d892a2b099b8c6edfd1123ce58a34458ee20cad676a7f7cfd80a28f0cb0888af88838310db372986bdcf9bfcae2324480ca7360d22bff21fb569a530e" } , { } }; // if the return type (blobdata for now) of block_to_blob ever changes // from std::string, this might break. bool compare_blocks(const block& a, const block& b) { auto hash_a = pod_to_hex(get_block_hash(a)); auto hash_b = pod_to_hex(get_block_hash(b)); return hash_a == hash_b; } /* void print_block(const block& blk, const std::string& prefix = "") { std::cerr << prefix << ": " << std::endl << "\thash - " << pod_to_hex(get_block_hash(blk)) << std::endl << "\tparent - " << pod_to_hex(blk.prev_id) << std::endl << "\ttimestamp - " << blk.timestamp << std::endl ; } // if the return type (blobdata for now) of tx_to_blob ever changes // from std::string, this might break. bool compare_txs(const transaction& a, const transaction& b) { auto ab = tx_to_blob(a); auto bb = tx_to_blob(b); return ab == bb; } */ // convert hex string to string that has values based on that hex // thankfully should automatically ignore null-terminator. std::string h2b(const std::string& s) { bool upper = true; std::string result; unsigned char val = 0; for (char c : s) { if (upper) { val = 0; if (c <= 'f' && c >= 'a') { val = ((c - 'a') + 10) << 4; } else { val = (c - '0') << 4; } } else { if (c <= 'f' && c >= 'a') { val |= (c - 'a') + 10; } else { val |= c - '0'; } result += (char)val; } upper = !upper; } return result; } template class BlockchainDBTest : public testing::Test { protected: BlockchainDBTest() : m_db(new T()), m_hardfork(*m_db, 1, 0) { for (auto& i : t_blocks) { block bl; blobdata bd = h2b(i); CHECK_AND_ASSERT_THROW_MES(parse_and_validate_block_from_blob(bd, bl), "Invalid block"); m_blocks.push_back(std::make_pair(bl, bd)); } for (auto& i : t_transactions) { std::vector> txs; for (auto& j : i) { transaction tx; blobdata bd = h2b(j); CHECK_AND_ASSERT_THROW_MES(parse_and_validate_tx_from_blob(bd, tx), "Invalid transaction"); txs.push_back(std::make_pair(tx, bd)); } m_txs.push_back(txs); } } ~BlockchainDBTest() { delete m_db; remove_files(); } BlockchainDB* m_db; HardFork m_hardfork; std::string m_prefix; std::vector> m_blocks; std::vector>> m_txs; std::vector m_filenames; void init_hard_fork() { m_hardfork.init(); m_db->set_hard_fork(&m_hardfork); } void get_filenames() { m_filenames = m_db->get_filenames(); for (auto& f : m_filenames) { std::cerr << "File created by test: " << f << std::endl; } } void remove_files() { // remove each file the db created, making sure it starts with fname. for (auto& f : m_filenames) { if (boost::starts_with(f, m_prefix)) { boost::filesystem::remove(f); } else { std::cerr << "File created by test not to be removed (for safety): " << f << std::endl; } } // remove directory if it still exists boost::filesystem::remove_all(m_prefix); } void set_prefix(const std::string& prefix) { m_prefix = prefix; } }; using testing::Types; typedef Types implementations; TYPED_TEST_CASE(BlockchainDBTest, implementations); TYPED_TEST(BlockchainDBTest, OpenAndClose) { boost::filesystem::path tempPath = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); std::string dirPath = tempPath.string(); this->set_prefix(dirPath); // make sure open does not throw ASSERT_NO_THROW(this->m_db->open(dirPath)); this->get_filenames(); // make sure open when already open DOES throw ASSERT_THROW(this->m_db->open(dirPath), DB_OPEN_FAILURE); ASSERT_NO_THROW(this->m_db->close()); } TYPED_TEST(BlockchainDBTest, AssetRecordsPersistAndRollbackAtomically) { const boost::filesystem::path temp_path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); const std::string dir_path = temp_path.string(); this->set_prefix(dir_path); ASSERT_NO_THROW(this->m_db->open(dir_path)); this->get_filenames(); crypto::hash first{}, aborted{}, later{}; reinterpret_cast(&first)[0] = 1; reinterpret_cast(&aborted)[0] = 2; reinterpret_cast(&later)[0] = 3; const blobdata first_payload("signed issuance one"); const blobdata aborted_payload("must not survive abort"); const blobdata later_payload("signed issuance later"); ASSERT_NO_THROW(this->m_db->add_asset_record(first, 7, blobdata_ref(first_payload))); ASSERT_THROW(this->m_db->add_asset_record(first, 8, blobdata_ref(first_payload)), DB_ERROR); this->m_db->block_wtxn_start(); ASSERT_NO_THROW(this->m_db->add_asset_record(aborted, 8, blobdata_ref(aborted_payload))); this->m_db->block_wtxn_abort(); uint64_t height = 0; blobdata payload; ASSERT_FALSE(this->m_db->get_asset_record(aborted, height, payload)); ASSERT_NO_THROW(this->m_db->add_asset_record(aborted, 10, blobdata_ref(aborted_payload))); ASSERT_NO_THROW(this->m_db->add_asset_record(later, 11, blobdata_ref(later_payload))); ASSERT_NO_THROW(this->m_db->close()); ASSERT_NO_THROW(this->m_db->open(dir_path)); ASSERT_TRUE(this->m_db->get_asset_record(first, height, payload)); ASSERT_EQ(7u, height); ASSERT_EQ(first_payload, payload); size_t count = 0; ASSERT_TRUE(this->m_db->for_all_asset_records([&count](const crypto::hash&, uint64_t, const blobdata_ref&) { ++count; return true; })); ASSERT_EQ(3u, count); ASSERT_NO_THROW(this->m_db->remove_asset_records_from_height(10)); ASSERT_TRUE(this->m_db->get_asset_record(first, height, payload)); ASSERT_FALSE(this->m_db->get_asset_record(aborted, height, payload)); ASSERT_FALSE(this->m_db->get_asset_record(later, height, payload)); } TYPED_TEST(BlockchainDBTest, AssetOutputsAndKeyImagesPersistAndRollbackAtomically) { const boost::filesystem::path temp_path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); const std::string dir_path = temp_path.string(); this->set_prefix(dir_path); ASSERT_NO_THROW(this->m_db->open(dir_path)); this->get_filenames(); crypto::hash first_id{}, later_id{}, aborted_id{}, asset_id{}; first_id.data[0] = 1; later_id.data[0] = 2; aborted_id.data[0] = 3; asset_id.data[0] = 0xa7; rct::key first_secret{}, first_key{}, later_key{}, aborted_key{}; rct::skpkGen(first_secret, first_key); rct::skpkGen(first_secret, later_key); rct::skpkGen(first_secret, aborted_key); asset_output_data_t first{asset_id, first_key, rct::commit(11, rct::zero()), 30}; asset_output_data_t later{asset_id, later_key, rct::commit(12, rct::zero()), 31}; asset_output_data_t aborted{asset_id, aborted_key, rct::commit(13, rct::zero()), 32}; crypto::key_image spent{}, aborted_spent{}; reinterpret_cast(&spent)[0] = 0x41; reinterpret_cast(&aborted_spent)[0] = 0x42; ASSERT_NO_THROW(this->m_db->add_asset_output(first_id, first)); ASSERT_THROW(this->m_db->add_asset_output(first_id, first), DB_ERROR); ASSERT_NO_THROW(this->m_db->add_asset_output(later_id, later)); ASSERT_NO_THROW(this->m_db->add_asset_key_image(spent, 31)); ASSERT_THROW(this->m_db->add_asset_key_image(spent, 31), KEY_IMAGE_EXISTS); this->m_db->block_wtxn_start(); ASSERT_NO_THROW(this->m_db->add_asset_output(aborted_id, aborted)); ASSERT_NO_THROW(this->m_db->add_asset_key_image(aborted_spent, 32)); this->m_db->block_wtxn_abort(); asset_output_data_t restored{}; ASSERT_FALSE(this->m_db->get_asset_output(aborted_id, restored)); ASSERT_FALSE(this->m_db->has_asset_key_image(aborted_spent)); ASSERT_NO_THROW(this->m_db->close()); ASSERT_NO_THROW(this->m_db->open(dir_path)); ASSERT_TRUE(this->m_db->get_asset_output(first_id, restored)); ASSERT_EQ(first.asset_id, restored.asset_id); ASSERT_EQ(first.destination, restored.destination); ASSERT_EQ(first.commitment, restored.commitment); ASSERT_EQ(first.height, restored.height); ASSERT_TRUE(this->m_db->has_asset_key_image(spent)); ASSERT_NO_THROW(this->m_db->remove_asset_outputs_from_height(31)); ASSERT_NO_THROW(this->m_db->remove_asset_key_images_from_height(31)); ASSERT_TRUE(this->m_db->get_asset_output(first_id, restored)); ASSERT_FALSE(this->m_db->get_asset_output(later_id, restored)); ASSERT_FALSE(this->m_db->has_asset_key_image(spent)); } TYPED_TEST(BlockchainDBTest, AssetOwnershipResolvesAuthoritativeRingAndSpentState) { const boost::filesystem::path temp_path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); const std::string dir_path = temp_path.string(); this->set_prefix(dir_path); ASSERT_NO_THROW(this->m_db->open(dir_path)); this->get_filenames(); crypto::hash id{}, carrier{}; id.data[0] = 0x91; carrier.data[0] = 0x92; const auto proof = make_db_ownership_proof(id, carrier); std::string error; EXPECT_FALSE(assets::verify_asset_ownership_against_db( *this->m_db, proof, TESTNET, carrier, &error)); for (const auto& member : proof.ring) { asset_output_data_t output{member.asset_id, member.public_output.dest, member.public_output.mask, 40}; ASSERT_NO_THROW(this->m_db->add_asset_output(member.output_id, output)); } ASSERT_TRUE(assets::verify_asset_ownership_against_db( *this->m_db, proof, TESTNET, carrier, &error)) << error; ASSERT_NO_THROW(this->m_db->add_asset_key_image(proof.key_image, 41)); EXPECT_FALSE(assets::verify_asset_ownership_against_db( *this->m_db, proof, TESTNET, carrier, &error)); ASSERT_NO_THROW(this->m_db->remove_asset_key_images_from_height(41)); ASSERT_TRUE(assets::verify_asset_ownership_against_db( *this->m_db, proof, TESTNET, carrier, &error)) << error; ASSERT_NO_THROW(this->m_db->remove_asset_outputs_from_height(40)); EXPECT_FALSE(assets::verify_asset_ownership_against_db( *this->m_db, proof, TESTNET, carrier, &error)); } TYPED_TEST(BlockchainDBTest, AssetTransactionStateAppliesAtomicallyAndRejectsReplay) { const boost::filesystem::path temp_path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); const std::string dir_path = temp_path.string(); this->set_prefix(dir_path); ASSERT_NO_THROW(this->m_db->open(dir_path)); this->get_filenames(); crypto::hash carrier{}; carrier.data[0] = 0xd1; const auto payload = make_db_asset_issuance(carrier); std::vector output_ids; std::string error; this->m_db->block_wtxn_start(); ASSERT_TRUE(assets::apply_asset_transaction_to_db(*this->m_db, payload, TESTNET, carrier, 50, output_ids, &error)) << error; this->m_db->block_wtxn_stop(); ASSERT_EQ(1u, output_ids.size()); crypto::hash asset_id{}; ASSERT_TRUE(assets::derive_asset_id(payload.issuance->descriptor, asset_id)); uint64_t issuance_height = 0; blobdata issuance_bytes; ASSERT_TRUE(this->m_db->get_asset_record(asset_id, issuance_height, issuance_bytes)); ASSERT_EQ(50u, issuance_height); asset_output_data_t stored{}; ASSERT_TRUE(this->m_db->get_asset_output(output_ids.front(), stored)); ASSERT_EQ(asset_id, stored.asset_id); ASSERT_EQ(50u, stored.height); std::vector replay_outputs; this->m_db->block_wtxn_start(); EXPECT_FALSE(assets::apply_asset_transaction_to_db(*this->m_db, payload, TESTNET, carrier, 51, replay_outputs, &error)); this->m_db->block_wtxn_abort(); ASSERT_TRUE(replay_outputs.empty()); auto invalid = make_db_asset_issuance(crypto::hash{}); crypto::hash invalid_carrier{}; invalid_carrier.data[0] = 0xd2; invalid.carrier_prefix_hash = invalid_carrier; invalid.balances.front().outputs.front() = rct::commit( invalid.issuance->descriptor.atomic_supply + 1, rct::zero()); this->m_db->block_wtxn_start(); EXPECT_FALSE(assets::apply_asset_transaction_to_db(*this->m_db, invalid, TESTNET, invalid_carrier, 52, replay_outputs, &error)); this->m_db->block_wtxn_abort(); crypto::hash invalid_id{}; ASSERT_TRUE(assets::derive_asset_id(invalid.issuance->descriptor, invalid_id)); ASSERT_FALSE(this->m_db->get_asset_record(invalid_id, issuance_height, issuance_bytes)); } TYPED_TEST(BlockchainDBTest, AssetBlockExtensionsRebuildFromPersistentState) { const boost::filesystem::path temp_path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); const std::string dir_path = temp_path.string(); this->set_prefix(dir_path); ASSERT_NO_THROW(this->m_db->open(dir_path)); this->get_filenames(); crypto::hash first_carrier{}, second_carrier{}; first_carrier.data[0] = 0x31; second_carrier.data[0] = 0x32; const auto first = make_asset_extension(first_carrier); const auto second = make_asset_extension(second_carrier); std::vector ids; std::string error; this->m_db->block_wtxn_start(); ASSERT_TRUE(assets::apply_block_extensions_to_db(*this->m_db, {first}, {first_carrier}, TESTNET, 20, ids, &error)) << error; this->m_db->block_wtxn_stop(); ASSERT_EQ(1u, ids.size()); this->m_db->block_wtxn_start(); ASSERT_TRUE(assets::apply_block_extensions_to_db(*this->m_db, {second}, {second_carrier}, TESTNET, 21, ids, &error)) << error; this->m_db->block_wtxn_stop(); assets::asset_registry registry; ASSERT_TRUE(assets::load_registry_from_db(*this->m_db, TESTNET, registry, &error)) << error; ASSERT_EQ(2u, registry.size()); ASSERT_FALSE(assets::load_registry_from_db(*this->m_db, MAINNET, registry, &error)); ASSERT_NO_THROW(this->m_db->remove_asset_records_from_height(21)); ASSERT_TRUE(assets::load_registry_from_db(*this->m_db, TESTNET, registry, &error)) << error; ASSERT_EQ(1u, registry.size()); auto invalid = second; invalid.carrier_prefix_hash.data[1] ^= 1; this->m_db->block_wtxn_start(); ASSERT_FALSE(assets::apply_block_extensions_to_db(*this->m_db, {invalid}, {second_carrier}, TESTNET, 22, ids, &error)); this->m_db->block_wtxn_abort(); ASSERT_TRUE(assets::load_registry_from_db(*this->m_db, TESTNET, registry, &error)); ASSERT_EQ(1u, registry.size()); } TYPED_TEST(BlockchainDBTest, PopBlockRemovesAssetStateAtDetachedHeight) { const boost::filesystem::path temp_path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); const std::string dir_path = temp_path.string(); this->set_prefix(dir_path); ASSERT_NO_THROW(this->m_db->open(dir_path)); this->get_filenames(); this->init_hard_fork(); crypto::hash id{}; crypto::hash output_id{}; crypto::key_image spent{}; id.data[0] = 0xa5; output_id.data[0] = 0xa6; reinterpret_cast(&spent)[0] = 0xa7; const blobdata payload("detached asset"); rct::key output_key{}, output_secret{}; rct::skpkGen(output_secret, output_key); const asset_output_data_t asset_output{id, output_key, rct::commit(50, rct::zero()), 1}; block popped; std::vector transactions; { db_wtxn_guard guard(this->m_db); ASSERT_NO_THROW(this->m_db->add_block(this->m_blocks[0], t_sizes[0], t_sizes[0], t_diffs[0], t_coins[0], this->m_txs[0])); ASSERT_NO_THROW(this->m_db->add_block(this->m_blocks[1], t_sizes[1], t_sizes[1], t_diffs[1], t_coins[1], this->m_txs[1])); ASSERT_NO_THROW(this->m_db->add_asset_record(id, 1, blobdata_ref(payload))); ASSERT_NO_THROW(this->m_db->add_asset_output(output_id, asset_output)); ASSERT_NO_THROW(this->m_db->add_asset_key_image(spent, 1)); } ASSERT_NO_THROW(this->m_db->pop_block(popped, transactions)); uint64_t height = 0; blobdata restored; asset_output_data_t restored_output{}; ASSERT_FALSE(this->m_db->get_asset_record(id, height, restored)); ASSERT_FALSE(this->m_db->get_asset_output(output_id, restored_output)); ASSERT_FALSE(this->m_db->has_asset_key_image(spent)); ASSERT_EQ(1u, this->m_db->height()); } TYPED_TEST(BlockchainDBTest, AddBlock) { boost::filesystem::path tempPath = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); std::string dirPath = tempPath.string(); this->set_prefix(dirPath); // make sure open does not throw ASSERT_NO_THROW(this->m_db->open(dirPath)); this->get_filenames(); this->init_hard_fork(); db_wtxn_guard guard(this->m_db); // adding a block with no parent in the blockchain should throw. // note: this shouldn't be possible, but is a good (and cheap) failsafe. // // TODO: need at least one more block to make this reasonable, as the // BlockchainDB implementation should not check for parent if // no blocks have been added yet (because genesis has no parent). //ASSERT_THROW(this->m_db->add_block(this->m_blocks[1], t_sizes[1], t_sizes[1], t_diffs[1], t_coins[1], this->m_txs[1]), BLOCK_PARENT_DNE); ASSERT_NO_THROW(this->m_db->add_block(this->m_blocks[0], t_sizes[0], t_sizes[0], t_diffs[0], t_coins[0], this->m_txs[0])); ASSERT_NO_THROW(this->m_db->add_block(this->m_blocks[1], t_sizes[1], t_sizes[1], t_diffs[1], t_coins[1], this->m_txs[1])); block b; ASSERT_TRUE(this->m_db->block_exists(get_block_hash(this->m_blocks[0].first))); ASSERT_NO_THROW(b = this->m_db->get_block(get_block_hash(this->m_blocks[0].first))); ASSERT_TRUE(compare_blocks(this->m_blocks[0].first, b)); ASSERT_NO_THROW(b = this->m_db->get_block_from_height(0)); ASSERT_TRUE(compare_blocks(this->m_blocks[0].first, b)); // assert that we can't add the same block twice ASSERT_THROW(this->m_db->add_block(this->m_blocks[0], t_sizes[0], t_sizes[0], t_diffs[0], t_coins[0], this->m_txs[0]), TX_EXISTS); for (auto& h : this->m_blocks[0].first.tx_hashes) { transaction tx; ASSERT_TRUE(this->m_db->tx_exists(h)); ASSERT_NO_THROW(tx = this->m_db->get_tx(h)); ASSERT_HASH_EQ(h, get_transaction_hash(tx)); } } TYPED_TEST(BlockchainDBTest, RetrieveBlockData) { boost::filesystem::path tempPath = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); std::string dirPath = tempPath.string(); this->set_prefix(dirPath); // make sure open does not throw ASSERT_NO_THROW(this->m_db->open(dirPath)); this->get_filenames(); this->init_hard_fork(); db_wtxn_guard guard(this->m_db); ASSERT_NO_THROW(this->m_db->add_block(this->m_blocks[0], t_sizes[0], t_sizes[0], t_diffs[0], t_coins[0], this->m_txs[0])); ASSERT_EQ(t_sizes[0], this->m_db->get_block_weight(0)); ASSERT_EQ(t_diffs[0], this->m_db->get_block_cumulative_difficulty(0)); ASSERT_EQ(t_diffs[0], this->m_db->get_block_difficulty(0)); ASSERT_EQ(t_coins[0], this->m_db->get_block_already_generated_coins(0)); ASSERT_NO_THROW(this->m_db->add_block(this->m_blocks[1], t_sizes[1], t_sizes[1], t_diffs[1], t_coins[1], this->m_txs[1])); ASSERT_EQ(t_diffs[1] - t_diffs[0], this->m_db->get_block_difficulty(1)); ASSERT_HASH_EQ(get_block_hash(this->m_blocks[0].first), this->m_db->get_block_hash_from_height(0)); std::vector blks; ASSERT_NO_THROW(blks = this->m_db->get_blocks_range(0, 1)); ASSERT_EQ(2, blks.size()); ASSERT_HASH_EQ(get_block_hash(this->m_blocks[0].first), get_block_hash(blks[0])); ASSERT_HASH_EQ(get_block_hash(this->m_blocks[1].first), get_block_hash(blks[1])); std::vector hashes; ASSERT_NO_THROW(hashes = this->m_db->get_hashes_range(0, 1)); ASSERT_EQ(2, hashes.size()); ASSERT_HASH_EQ(get_block_hash(this->m_blocks[0].first), hashes[0]); ASSERT_HASH_EQ(get_block_hash(this->m_blocks[1].first), hashes[1]); } } // anonymous namespace