// Copyright (c) 2014-2024, 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 "blockchain_db/lmdb/db_lmdb.h" #include "cryptonote_basic/cryptonote_basic_impl.h" #include "hex.h" #include "lmdb/database.h" #include "lmdb/table.h" #include "lmdb/transaction.h" #include "lmdb/util.h" #include "string_tools.h" namespace { enum class choice : unsigned {}; enum class big_choice : unsigned long {}; struct bytes { char data[16]; }; MONERO_CURSOR(test_cursor); template int run_compare(T left, T right, MDB_cmp_func* cmp) { MDB_val left_val = lmdb::to_val(left); MDB_val right_val = lmdb::to_val(right); return (*cmp)(&left_val, &right_val); } crypto::hash postfix_hex_to_hash(const std::string& hex) { if (hex.size() > 64) throw std::logic_error("postfix_hex_to_hash"); std::string decoded_bytes; if (!epee::from_hex::to_string(decoded_bytes, hex)) throw std::logic_error("postfix_hex_to_hash"); crypto::hash res = crypto::null_hash; memcpy(res.data + 32 - decoded_bytes.size(), decoded_bytes.data(), decoded_bytes.size()); return res; } void test_make_template(const std::string& input_hex, unsigned int nbits, const std::string& expected_hex) { const crypto::hash input = postfix_hex_to_hash(input_hex); const crypto::hash expected = postfix_hex_to_hash(expected_hex); const crypto::hash actual = cryptonote::make_hash32_loose_template(nbits, input); ASSERT_EQ(expected, actual); } } TEST(LMDB, Traits) { EXPECT_TRUE((std::is_same::type>())); EXPECT_TRUE((std::is_same::type>())); EXPECT_TRUE((std::is_same>())); EXPECT_TRUE((std::is_same>())); EXPECT_TRUE((std::is_same>())); EXPECT_TRUE((std::is_same>())); } TEST(LMDB, ToNative) { enum class negative_choice : int {}; EXPECT_TRUE((std::is_same())); EXPECT_TRUE( (std::is_same()) ); EXPECT_TRUE( (std::is_same()) ); EXPECT_EQ(unsigned(0), lmdb::to_native(choice(0))); EXPECT_EQ(unsigned(0xffffffff), lmdb::to_native(choice(0xffffffff))); EXPECT_EQ(-1, lmdb::to_native(negative_choice(-1))); // test constexpr static_assert(100 == lmdb::to_native(choice(100)), "to_native failed"); static_assert(-100 == lmdb::to_native(negative_choice(-100)), "to_native failed"); } TEST(LMDB, Conversions) { struct one { big_choice i; choice j; }; const one test{big_choice(100), choice(95)}; one test2{big_choice(1000), choice(950)}; EXPECT_EQ(&test, lmdb::to_val(test).mv_data); EXPECT_NE(&test2, lmdb::to_val(test).mv_data); EXPECT_EQ( &test, static_cast(lmdb::to_byte_span(lmdb::to_val(test)).begin()) ); EXPECT_EQ(sizeof(test), lmdb::to_val(test).mv_size); EXPECT_EQ(sizeof(test), lmdb::to_byte_span(lmdb::to_val(test)).size()); EXPECT_EQ(&test2, lmdb::to_val(test2).mv_data); EXPECT_NE(&test, lmdb::to_val(test2).mv_data); EXPECT_EQ( &test2, static_cast(lmdb::to_byte_span(lmdb::to_val(test2)).begin()) ); EXPECT_EQ(sizeof(test2), lmdb::to_val(test2).mv_size); EXPECT_EQ(sizeof(test2), lmdb::to_byte_span(lmdb::to_val(test2)).size()); } TEST(LMDB, LessSort) { struct one { unsigned i; unsigned j; }; struct two { unsigned i; choice j; }; EXPECT_EQ(0, run_compare(0u, 0u, &lmdb::less)); EXPECT_EQ(-1, run_compare(0u, 1u, &lmdb::less)); EXPECT_EQ(1, run_compare(1u, 0u, &lmdb::less)); EXPECT_EQ(0, run_compare({0, 1}, {0, 1}, &lmdb::less)); EXPECT_EQ(-1, run_compare({0, 0}, {0, 1}, &lmdb::less)); EXPECT_EQ(1, run_compare({0, 1}, {0, 0}, &lmdb::less)); EXPECT_EQ(0, run_compare({0, 1}, {0, 1}, MONERO_SORT_BY(one, j))); EXPECT_EQ(-1, run_compare({0, 0}, {0, 1}, MONERO_SORT_BY(one, j))); EXPECT_EQ(1, run_compare({0, 1}, {0, 0}, MONERO_SORT_BY(one, j))); EXPECT_EQ(0, run_compare({0, choice(1)}, {0, choice(1)}, MONERO_SORT_BY(two, j))); EXPECT_EQ(-1, run_compare({0, choice(0)}, {0, choice(1)}, MONERO_SORT_BY(two, j))); EXPECT_EQ(1, run_compare({0, choice(1)}, {0, choice(0)}, MONERO_SORT_BY(two, j))); // compare function addresses EXPECT_EQ((MONERO_SORT_BY(one, i)), (MONERO_SORT_BY(two, i))); EXPECT_EQ((MONERO_SORT_BY(one, j)), (MONERO_SORT_BY(two, j))); EXPECT_NE((MONERO_SORT_BY(one, i)), (MONERO_SORT_BY(two, j))); EXPECT_NE((MONERO_SORT_BY(one, j)), (MONERO_SORT_BY(two, i))); } TEST(LMDB, SortCompare) { struct one { unsigned i; bytes j; }; one test{55}; boost::iota(test.j.data, 10); const one test2 = test; EXPECT_EQ(0, run_compare(test, test2, MONERO_COMPARE(one, j))); test.j.data[15] = 1; EXPECT_GT(0, run_compare(test, test2, MONERO_COMPARE(one, j))); test.j.data[15] = 100; EXPECT_LT(0, run_compare(test, test2, MONERO_COMPARE(one, j))); } TEST(LMDB, Table) { struct one { bytes i; bytes j; }; constexpr lmdb::basic_table test{"foo"}; EXPECT_STREQ("foo", test.name); static_assert(test.flags == 0, "bad flags"); static_assert(&lmdb::less == test.key_cmp, "bad key_cmp"); static_assert(test.value_cmp == nullptr, "bad value_cmp"); EXPECT_TRUE(test.get_value(MDB_val{}).matches(std::errc::invalid_argument)); lmdb::basic_table test2{ "foo2", MDB_DUPSORT, &lmdb::compare }; EXPECT_STREQ("foo2", test2.name); EXPECT_EQ((MDB_DUPSORT | MDB_DUPFIXED), test2.flags); EXPECT_EQ(&lmdb::less, test2.key_cmp); EXPECT_EQ(&lmdb::compare, test2.value_cmp); EXPECT_TRUE(test2.get_value(MDB_val{}).matches(std::errc::invalid_argument)); one record{}; boost::iota(record.i.data, 0); boost::iota(record.i.data, 20); const one record_copy = MONERO_UNWRAP(test2.get_value(lmdb::to_val(record))); EXPECT_TRUE(boost::equal(record.i.data, record_copy.i.data)); EXPECT_TRUE(boost::equal(record.j.data, record_copy.j.data)); const bytes j_copy = MONERO_UNWRAP( test2.get_value(lmdb::to_val(record)) ); EXPECT_TRUE(boost::equal(record.j.data, j_copy.data)); EXPECT_TRUE( test.get_key_stream(test_cursor{}).matches(std::errc::invalid_argument) ); EXPECT_TRUE( test2.get_key_stream(test_cursor{}).matches(std::errc::invalid_argument) ); EXPECT_TRUE( test.get_value_stream(choice(0), test_cursor{}).matches(std::errc::invalid_argument) ); EXPECT_TRUE( test2.get_value_stream(big_choice(0), test_cursor{}).matches(std::errc::invalid_argument) ); } TEST(LMDB, InvalidDatabase) { lmdb::database test{lmdb::environment{}}; EXPECT_TRUE(test.resize().matches(std::errc::invalid_argument)); EXPECT_TRUE(test.create_read_txn().matches(std::errc::invalid_argument)); EXPECT_TRUE(test.reset_txn(lmdb::read_txn{}).matches(std::errc::invalid_argument)); EXPECT_TRUE(test.create_write_txn().matches(std::errc::invalid_argument)); EXPECT_TRUE(test.commit(lmdb::write_txn{}).matches(std::errc::invalid_argument)); EXPECT_TRUE( test.try_write( [](MDB_txn&) { return success(); } ).matches(std::errc::invalid_argument) ); } TEST(LMDB, InvalidValueStream) { struct one { choice i; choice j; bytes k; }; lmdb::value_stream test{test_cursor{}}; EXPECT_TRUE((std::is_same())); EXPECT_TRUE((std::is_same())); EXPECT_TRUE( (std::is_same()))>()) ); EXPECT_TRUE( (std::is_same().begin()))>()) ); EXPECT_NO_THROW(test.reset()); EXPECT_EQ(0u, test.count()); EXPECT_TRUE(test.make_iterator().is_end()); EXPECT_TRUE(test.make_range().empty()); EXPECT_EQ(nullptr, test.give_cursor()); EXPECT_EQ(0u, test.count()); EXPECT_TRUE(test.make_iterator().is_end()); EXPECT_TRUE(test.make_range().empty()); EXPECT_EQ(nullptr, test.give_cursor()); } TEST(LMDB, InvalidValueIterator) { struct one { choice i; choice j; bytes k; }; lmdb::value_iterator test1{}; EXPECT_TRUE((std::is_same())); EXPECT_TRUE( (std::is_same())>()) ); EXPECT_TRUE(test1.is_end()); EXPECT_NO_THROW(++test1); EXPECT_NO_THROW(test1++); EXPECT_TRUE(test1.is_end()); lmdb::value_iterator test2{nullptr}; EXPECT_TRUE(test2.is_end()); EXPECT_NO_THROW(++test2); EXPECT_NO_THROW(test2++); EXPECT_TRUE(test2.is_end()); EXPECT_TRUE(test1.equal(test2)); EXPECT_TRUE(test2.equal(test1)); EXPECT_TRUE(test1 == test2); EXPECT_TRUE(test2 == test1); EXPECT_FALSE(test1 != test2); EXPECT_FALSE(test2 != test1); lmdb::value_iterator test3{}; EXPECT_TRUE((std::is_same())); EXPECT_TRUE((std::is_same())>())); EXPECT_TRUE( (std::is_same())>()) ); EXPECT_TRUE(test3.is_end()); EXPECT_NO_THROW(++test3); EXPECT_NO_THROW(test3++); EXPECT_TRUE(test3.is_end()); } TEST(LMDB, InvalidKeyStream) { struct one { choice i; choice j; bytes k; }; using record = std::pair>>; lmdb::key_stream test{test_cursor{}}; EXPECT_TRUE((std::is_same())); EXPECT_TRUE((std::is_same())); EXPECT_NO_THROW(test.reset()); EXPECT_TRUE(test.make_iterator().is_end()); EXPECT_TRUE(test.make_range().empty()); EXPECT_EQ(nullptr, test.give_cursor()); EXPECT_TRUE(test.make_iterator().is_end()); EXPECT_TRUE(test.make_range().empty()); EXPECT_EQ(nullptr, test.give_cursor()); } TEST(LMDB, InvalidKeyIterator) { struct one { choice i; choice j; bytes k; }; using record = std::pair>>; lmdb::key_iterator test1{}; EXPECT_TRUE((std::is_same())); EXPECT_TRUE((std::is_same())); EXPECT_TRUE((std::is_same())); EXPECT_TRUE((std::is_same())); EXPECT_TRUE( (std::is_same()))>()) ); EXPECT_TRUE( (std::is_same().begin()))>()) ); EXPECT_TRUE(test1.is_end()); EXPECT_NO_THROW(++test1); EXPECT_NO_THROW(test1++); EXPECT_TRUE(test1.is_end()); EXPECT_TRUE(test1.make_value_iterator().is_end()); EXPECT_TRUE(test1.make_value_range().empty()); lmdb::key_iterator test2{nullptr}; EXPECT_TRUE(test2.is_end()); EXPECT_NO_THROW(++test2); EXPECT_NO_THROW(test2++); EXPECT_TRUE(test2.is_end()); EXPECT_TRUE(test2.make_value_iterator().is_end()); EXPECT_TRUE(test2.make_value_range().empty()); EXPECT_TRUE(test1.equal(test2)); EXPECT_TRUE(test2.equal(test1)); EXPECT_TRUE(test1 == test2); EXPECT_TRUE(test2 == test1); EXPECT_FALSE(test1 != test2); EXPECT_FALSE(test2 != test1); } TEST(LMDB_kanonymity, compare_hash32_reversed_nbits) { static constexpr size_t NUM_RANDOM_HASHES = 128; std::vector random_hashes; random_hashes.reserve(500); for (size_t i = 0; i < NUM_RANDOM_HASHES; ++i) random_hashes.push_back(crypto::rand()); bool r = true; // Compare behavior of compare_hash32_reversed_nbits(nbits=256) to BlockchainLMDB::compare_hash32 for (size_t i = 0; i < NUM_RANDOM_HASHES; ++i) { for (size_t j = 0; j < NUM_RANDOM_HASHES; ++j) { const crypto::hash& ha = random_hashes[i]; const crypto::hash& hb = random_hashes[j]; const MDB_val mva = {sizeof(crypto::hash), (void*)(&ha)}; const MDB_val mvb = {sizeof(crypto::hash), (void*)(&hb)}; const int expected = cryptonote::BlockchainLMDB::compare_hash32(&mva, &mvb); const int actual = cryptonote::compare_hash32_reversed_nbits(ha, hb, 256); if (actual != expected) { std::cerr << "Failed compare_hash32_reversed_nbits test case with hashes:" << std::endl; std::cerr << " " << epee::string_tools::pod_to_hex(ha) << std::endl; std::cerr << " " << epee::string_tools::pod_to_hex(hb) << std::endl; r = false; } EXPECT_EQ(expected, actual); } } ASSERT_TRUE(r); const auto cmp_byte_rev = [](const crypto::hash& ha, const crypto::hash& hb, unsigned int nbytes) -> int { if (nbytes > sizeof(crypto::hash)) throw std::logic_error("can't compare with nbytes too big"); const uint8_t* va = (const uint8_t*)ha.data; const uint8_t* vb = (const uint8_t*)hb.data; for (size_t i = 31; nbytes; --i, --nbytes) { if (va[i] < vb[i]) return -1; else if (va[i] > vb[i]) return 1; } return 0; }; // Test partial hash compares w/o partial bytes for (size_t i = 0; i < NUM_RANDOM_HASHES; ++i) { for (size_t j = 0; j < NUM_RANDOM_HASHES; ++j) { for (unsigned int nbytes = 0; nbytes <= 32; ++nbytes) { const crypto::hash& ha = random_hashes[i]; const crypto::hash& hb = random_hashes[j]; const int expected = cmp_byte_rev(ha, hb, nbytes); const int actual = cryptonote::compare_hash32_reversed_nbits(ha, hb, nbytes * 8); if (actual != expected) { std::cerr << "Failed compare_hash32_reversed_nbits test case with hashes and args:" << std::endl; std::cerr << " " << epee::string_tools::pod_to_hex(ha) << std::endl; std::cerr << " " << epee::string_tools::pod_to_hex(hb) << std::endl; std::cerr << " nbytes=" << nbytes << std::endl; r = false; } EXPECT_EQ(expected, actual); } } } ASSERT_TRUE(r); // Test partial hash compares w/ partial bytes for (size_t i = 0; i < NUM_RANDOM_HASHES; ++i) { const crypto::hash& ha = random_hashes[i]; for (size_t modnbytes = 0; modnbytes < 32; ++modnbytes) { for (size_t modbitpos = 0; modbitpos < 8; ++modbitpos) { const size_t modbytepos = 31 - modnbytes; const uint8_t mask = 1 << modbitpos; const bool bit_was_zero = 0 == (static_cast(ha.data[modbytepos]) & mask); const unsigned int modnbits = modnbytes * 8 + (7 - modbitpos); // Create modified random hash by flipping one bit crypto::hash hb = ha; hb.data[modbytepos] = static_cast(hb.data[modbytepos]) ^ mask; for (unsigned int cmpnbits = 0; cmpnbits <= 256; ++cmpnbits) { const int expected = cmpnbits <= modnbits ? 0 : bit_was_zero ? -1 : 1; const int actual = cryptonote::compare_hash32_reversed_nbits(ha, hb, cmpnbits); if (actual != expected) { std::cerr << "Failed compare_hash32_reversed_nbits test case with hashes and args:" << std::endl; std::cerr << " " << epee::string_tools::pod_to_hex(ha) << std::endl; std::cerr << " " << epee::string_tools::pod_to_hex(hb) << std::endl; std::cerr << " modnbytes=" << modnbytes << std::endl; std::cerr << " modbitpos=" << modbitpos << std::endl; std::cerr << " cmpnbits=" << cmpnbits << std::endl; r = false; } EXPECT_EQ(expected, actual); } } } } ASSERT_TRUE(r); // Test equality for (size_t i = 0; i < NUM_RANDOM_HASHES; ++i) { const crypto::hash& ha = random_hashes[i]; for (unsigned int nbits = 0; nbits <= 256; ++nbits) { const int actual = cryptonote::compare_hash32_reversed_nbits(ha, ha, nbits); if (actual) { std::cerr << "Failed compare_hash32_reversed_nbits test case with hash and args:" << std::endl; std::cerr << " " << epee::string_tools::pod_to_hex(ha) << std::endl; std::cerr << " nbits=" << nbits << std::endl; r = false; } EXPECT_EQ(0, actual); } } } TEST(LMDB_kanonymity, make_hash32_loose_template) { const std::string example_1 = "0abcdef1234567890abcdef1234567890abcdef1234567890abcdef123456789"; test_make_template(example_1, 0, ""); test_make_template(example_1, 1, "80"); test_make_template(example_1, 2, "80"); test_make_template(example_1, 3, "80"); test_make_template(example_1, 4, "80"); test_make_template(example_1, 5, "88"); test_make_template(example_1, 6, "88"); test_make_template(example_1, 7, "88"); test_make_template(example_1, 8, "89"); test_make_template(example_1, 9, "0089"); test_make_template(example_1, 10, "4089"); test_make_template(example_1, 11, "6089"); test_make_template(example_1, 12, "6089"); test_make_template(example_1, 13, "6089"); test_make_template(example_1, 14, "6489"); test_make_template(example_1, 15, "6689"); test_make_template(example_1, 16, "6789"); test_make_template(example_1, 32, "23456789"); test_make_template(example_1, 64, "0abcdef123456789"); test_make_template(example_1, 128, "0abcdef1234567890abcdef123456789"); test_make_template(example_1, 256, example_1); }