Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions include/bitcoin/database/impl/query/address/address_history.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ history CLASS::get_tx_history(hash_digest&& key, const tx_link& link,
auto height = history::unrooted_height;
auto position = history::unconfirmed_position;

const auto block = find_strong(link);
tx_link strong{};
const auto block = find_strong(strong, link);
const auto at = get_confirmed_height(block);
if (at.is_terminal())
{
Expand All @@ -153,7 +154,7 @@ history CLASS::get_tx_history(hash_digest&& key, const tx_link& link,
else
{
height = at.value;
if (!get_tx_position(position, link, block))
if (!get_tx_position(position, strong, block))
return {};
}

Expand All @@ -176,15 +177,16 @@ history CLASS::get_tx_confirmed_history(hash_digest&& key, const tx_link& link,
if (link.is_terminal())
return {};

const auto block = find_strong(link);
tx_link strong{};
const auto block = find_strong(strong, link);
const auto at = get_confirmed_height(block);

// Invalid with unconfirmed_position signals no fault.
if (at.is_terminal() || system::is_limited(at.value, start, end))
return { .position = history::unconfirmed_position };

size_t position{};
if (!get_tx_position(position, link, block))
if (!get_tx_position(position, strong, block))
return {};

// Electrum uses fees only on unconfirmed (expensive).
Expand Down
10 changes: 6 additions & 4 deletions include/bitcoin/database/impl/query/address/address_unspent.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,13 @@ unspent_output CLASS::get_tx_unspent(const output_link& link) const NOEXCEPT
auto height = unspent_output::unused_height;
auto position = unspent_output::unconfirmed_position;

const auto block = find_strong(tx);
tx_link strong{};
const auto block = find_strong(strong, tx);
const auto at = get_confirmed_height(block);
if (!at.is_terminal())
{
height = at.value;
if (!get_tx_position(position, tx, block))
if (!get_tx_position(position, strong, block))
return {};
}

Expand All @@ -147,7 +148,8 @@ unspent_output CLASS::get_tx_confirmed_unspent(const output_link& link) const NO
return {};

const auto tx = out.parent_fk;
const auto block = find_strong(tx);
tx_link strong{};
const auto block = find_strong(strong, tx);
const auto at = get_confirmed_height(block);

// Invalid with unconfirmed_position signals no fault.
Expand All @@ -158,7 +160,7 @@ unspent_output CLASS::get_tx_confirmed_unspent(const output_link& link) const NO
auto hash = get_tx_key(tx);
const auto index = to_output_index(tx, link);
if ((index == point::null_index) || (hash == system::null_hash) ||
!get_tx_position(position, tx, block))
!get_tx_position(position, strong, block))
return {};

return { { { std::move(hash), index }, out.value }, at.value, position };
Expand Down
27 changes: 25 additions & 2 deletions include/bitcoin/database/impl/query/consensus/consensus_strong.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,44 @@ height_link CLASS::find_strong_spender_height(

TEMPLATE
header_link CLASS::find_strong(const tx_link& link) const NOEXCEPT
{
tx_link unused{};
return find_strong(unused, link);
}

TEMPLATE
header_link CLASS::find_strong(tx_link& out, const tx_link& link) const NOEXCEPT
{
// Shortcuircuit hash-based search by testing self.
if (const auto fk = to_block(link); !fk.is_terminal())
{
out = link;
return fk;
}

return find_strong(get_tx_key(link));
return find_strong(out, get_tx_key(link));
}

TEMPLATE
header_link CLASS::find_strong(const hash_digest& tx_hash) const NOEXCEPT
{
// Find the first strong tx of the set and return its block.
tx_link unused{};
return find_strong(unused, tx_hash);
}

TEMPLATE
header_link CLASS::find_strong(tx_link& out,
const hash_digest& tx_hash) const NOEXCEPT
{
// Find the first strong tx of the set and return it with its block.
for (const auto& tx: to_duplicates(tx_hash))
{
if (const auto block = to_block(tx); !block.is_terminal())
{
out = tx;
return block;
}
}

return {};
}
Expand Down
4 changes: 3 additions & 1 deletion include/bitcoin/database/impl/query/properties_tx.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ bool CLASS::get_tx_height(size_t& out, const tx_link& link) const NOEXCEPT
TEMPLATE
bool CLASS::get_tx_position(size_t& out, const tx_link& link) const NOEXCEPT
{
return get_tx_position(out, link, find_strong(link));
tx_link strong{};
const auto block = find_strong(strong, link);
return get_tx_position(out, strong, block);
}

TEMPLATE
Expand Down
3 changes: 3 additions & 0 deletions include/bitcoin/database/query.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ class query
tx_link find_strong_tx(const hash_digest& tx_hash) const NOEXCEPT;
header_link find_strong(const tx_link& link) const NOEXCEPT;
header_link find_strong(const hash_digest& tx_hash) const NOEXCEPT;
header_link find_strong(tx_link& out, const tx_link& link) const NOEXCEPT;
header_link find_strong(tx_link& out,
const hash_digest& tx_hash) const NOEXCEPT;
height_link find_strong_spender_height(const point& point) const NOEXCEPT;

header_link to_parent(const header_link& link) const NOEXCEPT;
Expand Down
33 changes: 33 additions & 0 deletions test/query/address/address_balance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,37 @@ BOOST_AUTO_TEST_CASE(query_address__get_balance__block1a_address1_confirmed_bloc
BOOST_REQUIRE_EQUAL(unconfirmed, 0u);
}

// duplicate instances (one tx archived twice, only one instance associated)

BOOST_AUTO_TEST_CASE(query_address__get_balance__duplicate_instances__counted_once)
{
settings settings{};
settings.path = TEST_DIRECTORY;
test::chunk_store store{ settings };
test::query_accessor query{ store };
BOOST_REQUIRE(!store.create(test::events_handler));
BOOST_REQUIRE(!store.is_pooling());
BOOST_REQUIRE(query.initialize(test::genesis));
BOOST_REQUIRE(query.set(test::block1a, context{ 0, 1, 0 }, {}, false, false));
BOOST_REQUIRE(query.set_strong(1));
BOOST_REQUIRE(query.push_confirmed(1, false));
BOOST_REQUIRE(query.set(test::tx4));
BOOST_REQUIRE(query.set(test::block_spend_1a, context{ 0, 2, 0 }, {}, false, false));
BOOST_REQUIRE(query.set_strong(2));
BOOST_REQUIRE(query.push_confirmed(2, false));
BOOST_REQUIRE_EQUAL(query.to_duplicates(test::tx4.hash(false)).size(), 2u);

// block1a output 0 is spent by tx4, whose 0x08 output to the address is unspent.
uint64_t confirmed{};
uint64_t unconfirmed{};
const std::atomic_bool cancel{};
BOOST_REQUIRE_EQUAL(query.get_balance(cancel, confirmed, unconfirmed, test::block1a_address0, true), error::success);
BOOST_REQUIRE_EQUAL(confirmed, 0x08u);
BOOST_REQUIRE_EQUAL(unconfirmed, 0u);

uint64_t balance{};
BOOST_REQUIRE_EQUAL(query.get_confirmed_balance(cancel, balance, test::block1a_address0, true), error::success);
BOOST_REQUIRE_EQUAL(balance, 0x08u);
}

BOOST_AUTO_TEST_SUITE_END()
78 changes: 78 additions & 0 deletions test/query/address/address_history.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -569,4 +569,82 @@ BOOST_AUTO_TEST_CASE(query_address__get_spenders_history__confirmed__expected)
BOOST_CHECK_EQUAL(histories.at(3).tx.hash(), test::tx4.hash(false));
}

// duplicate instances (one tx archived twice, only one instance associated)

BOOST_AUTO_TEST_CASE(query_address__get_tx_history__duplicate_instances__same_entry)
{
settings settings{};
settings.path = TEST_DIRECTORY;
test::chunk_store store{ settings };
test::query_accessor query{ store };
BOOST_REQUIRE(!store.create(test::events_handler));
BOOST_REQUIRE(!store.is_pooling());
BOOST_REQUIRE(query.initialize(test::genesis));
BOOST_REQUIRE(query.set(test::block1a, context{ 0, 1, 0 }, {}, false, false));
BOOST_REQUIRE(query.set_strong(1));
BOOST_REQUIRE(query.push_confirmed(1, false));
BOOST_REQUIRE(query.set(test::tx4));
const auto loose = query.to_tx(test::tx4.hash(false));
BOOST_REQUIRE(query.set(test::block_spend_1a, context{ 0, 2, 0 }, {}, false, false));
BOOST_REQUIRE(query.set_strong(2));
BOOST_REQUIRE(query.push_confirmed(2, false));
const auto stored = query.get_position_tx(query.to_header(test::block_spend_1a.hash()), 0);
BOOST_REQUIRE(stored != loose);

const auto expected = query.get_tx_history(stored);
BOOST_REQUIRE(expected.valid());
BOOST_REQUIRE(expected.confirmed());
BOOST_REQUIRE_EQUAL(expected.tx.height(), 2u);
BOOST_REQUIRE_EQUAL(expected.position, 0u);

const auto duplicate = query.get_tx_history(loose);
BOOST_REQUIRE(!duplicate.fault());
BOOST_REQUIRE(duplicate.valid());
BOOST_REQUIRE(duplicate == expected);
BOOST_REQUIRE_EQUAL(duplicate.tx.height(), 2u);
BOOST_REQUIRE_EQUAL(duplicate.position, 0u);

const auto confirmed = query.get_tx_confirmed_history(loose);
BOOST_REQUIRE(!confirmed.fault());
BOOST_REQUIRE(confirmed == expected);
}

BOOST_AUTO_TEST_CASE(query_address__get_history__duplicate_instances__deduplicated)
{
settings settings{};
settings.path = TEST_DIRECTORY;
test::chunk_store store{ settings };
test::query_accessor query{ store };
BOOST_REQUIRE(!store.create(test::events_handler));
BOOST_REQUIRE(!store.is_pooling());
BOOST_REQUIRE(query.initialize(test::genesis));
BOOST_REQUIRE(query.set(test::block1a, context{ 0, 1, 0 }, {}, false, false));
BOOST_REQUIRE(query.set_strong(1));
BOOST_REQUIRE(query.push_confirmed(1, false));
BOOST_REQUIRE(query.set(test::tx4));
BOOST_REQUIRE(query.set(test::block_spend_1a, context{ 0, 2, 0 }, {}, false, false));
BOOST_REQUIRE(query.set_strong(2));
BOOST_REQUIRE(query.push_confirmed(2, false));
BOOST_REQUIRE_EQUAL(query.to_duplicates(test::tx4.hash(false)).size(), 2u);

histories out{};
height_link cursor{};
const std::atomic_bool cancel{};
BOOST_REQUIRE_EQUAL(query.get_history(cancel, cursor, out, test::block1a_address0, max_size_t, true), error::success);

// block1a coinbase (funds address0) and tx4 (spends it, pays address0), once each.
BOOST_REQUIRE_EQUAL(out.size(), 2u);
BOOST_REQUIRE_EQUAL(out.at(0).tx.hash(), test::block1a.transactions_ptr()->at(0)->hash(false));
BOOST_REQUIRE_EQUAL(out.at(0).tx.height(), 1u);
BOOST_REQUIRE_EQUAL(out.at(1).tx.hash(), test::tx4.hash(false));
BOOST_REQUIRE_EQUAL(out.at(1).tx.height(), 2u);
BOOST_REQUIRE_EQUAL(out.at(1).position, 0u);

histories confirmed{};
height_link confirmed_cursor{};
BOOST_REQUIRE_EQUAL(query.get_confirmed_history(cancel, confirmed_cursor, confirmed, test::block1a_address0, max_size_t, true), error::success);
BOOST_REQUIRE_EQUAL(confirmed.size(), 2u);
BOOST_REQUIRE_EQUAL(confirmed.at(1).tx.hash(), test::tx4.hash(false));
}

BOOST_AUTO_TEST_SUITE_END()
41 changes: 41 additions & 0 deletions test/query/address/address_unspent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,45 @@ BOOST_AUTO_TEST_CASE(query_address__get_unspent__turbo_genesis__expected)
BOOST_CHECK_EQUAL(out.at(5).out.point().hash(), test::block2b.transactions_ptr()->at(0)->hash(false));
}

// duplicate instances (one tx archived twice, only one instance associated)

BOOST_AUTO_TEST_CASE(query_address__get_unspent__duplicate_instances__deduplicated)
{
settings settings{};
settings.path = TEST_DIRECTORY;
test::chunk_store store{ settings };
test::query_accessor query{ store };
BOOST_REQUIRE(!store.create(test::events_handler));
BOOST_REQUIRE(!store.is_pooling());
BOOST_REQUIRE(query.initialize(test::genesis));
BOOST_REQUIRE(query.set(test::block1a, context{ 0, 1, 0 }, {}, false, false));
BOOST_REQUIRE(query.set_strong(1));
BOOST_REQUIRE(query.push_confirmed(1, false));
BOOST_REQUIRE(query.set(test::tx4));
BOOST_REQUIRE(query.set(test::block_spend_1a, context{ 0, 2, 0 }, {}, false, false));
BOOST_REQUIRE(query.set_strong(2));
BOOST_REQUIRE(query.push_confirmed(2, false));
BOOST_REQUIRE_EQUAL(query.to_duplicates(test::tx4.hash(false)).size(), 2u);

// block1a output 0 is spent by tx4, whose 0x08 output to the address is unspent.
unspent_outputs out{};
const std::atomic_bool cancel{};
BOOST_REQUIRE_EQUAL(query.get_unspent(cancel, out, test::block1a_address0, true), error::success);
BOOST_REQUIRE_EQUAL(out.size(), 1u);
BOOST_REQUIRE_EQUAL(out.at(0).out.point().hash(), test::tx4.hash(false));
BOOST_REQUIRE_EQUAL(out.at(0).out.point().index(), 0u);
BOOST_REQUIRE_EQUAL(out.at(0).out.value(), 0x08u);
BOOST_REQUIRE_EQUAL(out.at(0).height, 2u);
BOOST_REQUIRE_EQUAL(out.at(0).position, 0u);

unspent_outputs confirmed{};
BOOST_REQUIRE_EQUAL(query.get_confirmed_unspent(cancel, confirmed, test::block1a_address0, true), error::success);
BOOST_REQUIRE_EQUAL(confirmed.size(), 1u);
BOOST_REQUIRE_EQUAL(confirmed.at(0).out.point().hash(), test::tx4.hash(false));

unspent_outputs unconfirmed{};
BOOST_REQUIRE_EQUAL(query.get_unconfirmed_unspent(cancel, unconfirmed, test::block1a_address0, true), error::success);
BOOST_REQUIRE(unconfirmed.empty());
}

BOOST_AUTO_TEST_SUITE_END()
65 changes: 65 additions & 0 deletions test/query/consensus/consensus_strong.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,69 @@ BOOST_AUTO_TEST_CASE(query_consensus__set_strong__set_unstrong__expected)
BOOST_REQUIRE(!query.is_confirmed_output(query.to_output(2, 0)));
}

// duplicate instances (one tx archived twice, only one instance associated)

BOOST_AUTO_TEST_CASE(query_consensus__duplicate_instances__navigation__expected)
{
settings settings{};
settings.path = TEST_DIRECTORY;
test::chunk_store store{ settings };

class accessor
: public test::query_accessor
{
public:
using test::query_accessor::query_accessor;
using test::query_accessor::to_block;
};

accessor query{ store };
BOOST_REQUIRE(!store.create(test::events_handler));
BOOST_REQUIRE(!store.is_pooling());
BOOST_REQUIRE(query.initialize(test::genesis));
BOOST_REQUIRE(query.set(test::block1a, context{ 0, 1, 0 }, {}, false, false));
BOOST_REQUIRE(query.set_strong(1));
BOOST_REQUIRE(query.push_confirmed(1, false));

BOOST_REQUIRE(query.set(test::tx4));
const auto loose = query.to_tx(test::tx4.hash(false));
BOOST_REQUIRE(!loose.is_terminal());

BOOST_REQUIRE(query.set(test::block_spend_1a, context{ 0, 2, 0 }, {}, false, false));
BOOST_REQUIRE(query.set_strong(2));
BOOST_REQUIRE(query.push_confirmed(2, false));

const auto block = query.to_header(test::block_spend_1a.hash());
const auto stored = query.get_position_tx(block, 0);
BOOST_REQUIRE(!stored.is_terminal());
BOOST_REQUIRE(stored != loose);
BOOST_REQUIRE_EQUAL(query.get_tx_key(stored), test::tx4.hash(false));
BOOST_REQUIRE_EQUAL(query.to_duplicates(test::tx4.hash(false)).size(), 2u);

// The association is instance-exact.
BOOST_REQUIRE(query.to_block(loose).is_terminal());
BOOST_REQUIRE_EQUAL(query.to_block(stored), block);

// Strong resolution is by identity, from any instance.
BOOST_REQUIRE_EQUAL(query.find_strong(loose), block);
BOOST_REQUIRE_EQUAL(query.find_strong(stored), block);
BOOST_REQUIRE_EQUAL(query.find_strong_tx(loose), stored);
BOOST_REQUIRE_EQUAL(query.find_strong_tx(stored), stored);
BOOST_REQUIRE(query.is_confirmed_tx(loose));
BOOST_REQUIRE(query.is_confirmed_tx(stored));

// Confirmed properties are of the identity, from any instance.
size_t height{};
BOOST_REQUIRE(query.get_tx_height(height, loose));
BOOST_REQUIRE_EQUAL(height, 2u);
BOOST_REQUIRE(query.get_tx_height(height, stored));
BOOST_REQUIRE_EQUAL(height, 2u);

size_t position{};
BOOST_REQUIRE(query.get_tx_position(position, stored));
BOOST_REQUIRE_EQUAL(position, 0u);
BOOST_REQUIRE(query.get_tx_position(position, loose));
BOOST_REQUIRE_EQUAL(position, 0u);
}

BOOST_AUTO_TEST_SUITE_END()
Loading