diff --git a/include/bitcoin/node/chase.hpp b/include/bitcoin/node/chase.hpp index a53aad13..aac0df3a 100644 --- a/include/bitcoin/node/chase.hpp +++ b/include/bitcoin/node/chase.hpp @@ -153,6 +153,10 @@ enum class chase /// Issued by 'confirm' and handled by 'protocol_header/block_out/estimator'. block, + /// The store has been pruned (once, following the first current block). + /// Issued by 'snapshot' and handled by 'estimate'. + pruned, + /// The confirmed chain is no longer current. /// Issued by 'confirm' and handled by 'protocol_transaction_out'. stale, @@ -325,6 +329,12 @@ struct block header_t link; }; +struct pruned +{ + static constexpr chase id{ chase::pruned }; + header_t link; +}; + struct stale { static constexpr chase id{ chase::stale }; @@ -388,6 +398,7 @@ using event_value = std::variant chases::confirmable, chases::unconfirmable, chases::block, + chases::pruned, chases::stale, chases::organized, chases::reorganized, diff --git a/include/bitcoin/node/chasers/chaser_estimate.hpp b/include/bitcoin/node/chasers/chaser_estimate.hpp index e9e92089..10b17836 100644 --- a/include/bitcoin/node/chasers/chaser_estimate.hpp +++ b/include/bitcoin/node/chasers/chaser_estimate.hpp @@ -54,6 +54,7 @@ class BCN_API chaser_estimate protected: virtual bool handle_chase(const code& ec, event_value value) NOEXCEPT; + virtual void do_pruned(header_t link) NOEXCEPT; virtual void do_initialize(header_t link) NOEXCEPT; virtual void do_organized(header_t link) NOEXCEPT; virtual void do_reorganized(header_t link) NOEXCEPT; @@ -67,8 +68,9 @@ class BCN_API chaser_estimate std::atomic_bool stopping_{}; std::atomic_bool initialized_{}; - // This is protected by strand. + // These are protected by strand. estimator::ptr estimator_{}; + bool pruned_{}; }; } // namespace node diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index 78df26b4..a47d39de 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -119,8 +119,9 @@ class BCN_API chaser_validate bool stranded() const NOEXCEPT override; private: - using atomic_counter = std::atomic; using atomic_height = std::atomic; + using atomic_counter = std::atomic; + using bank_heights = std::array; struct counters { atomic_counter ecdsa_{}; @@ -175,7 +176,7 @@ class BCN_API chaser_validate std::atomic_bool silent_draining_{}; std::atomic_bool silent_bank_{}; std::array silent_writers_{}; - std::array silent_heights_{ max_size_t, max_size_t }; + bank_heights silent_heights_{ max_size_t, max_size_t }; counters counters_{}; stopper stopping_{}; diff --git a/include/bitcoin/node/protocols/protocol_transaction_in_106.hpp b/include/bitcoin/node/protocols/protocol_transaction_in_106.hpp index c6731488..e60e8d50 100644 --- a/include/bitcoin/node/protocols/protocol_transaction_in_106.hpp +++ b/include/bitcoin/node/protocols/protocol_transaction_in_106.hpp @@ -56,7 +56,7 @@ class BCN_API protocol_transaction_in_106 virtual type_id get_data_type() const NOEXCEPT; /// The hash by which the peer identifies the tx. - virtual system::hash_digest identifier( + virtual system::hash_digest tx_identifier( const system::chain::transaction& tx) const NOEXCEPT; /// The tx identified by the hash is archived. diff --git a/include/bitcoin/node/protocols/protocol_transaction_in_70016.hpp b/include/bitcoin/node/protocols/protocol_transaction_in_70016.hpp index cd2004b2..f0f37e15 100644 --- a/include/bitcoin/node/protocols/protocol_transaction_in_70016.hpp +++ b/include/bitcoin/node/protocols/protocol_transaction_in_70016.hpp @@ -43,7 +43,7 @@ class BCN_API protocol_transaction_in_70016 /// Transactions are announced, requested and identified by witness hash. type_id inventory_type() const NOEXCEPT override; type_id get_data_type() const NOEXCEPT override; - system::hash_digest identifier( + system::hash_digest tx_identifier( const system::chain::transaction& tx) const NOEXCEPT override; bool is_archived(const system::hash_digest& hash) const NOEXCEPT override; }; diff --git a/include/bitcoin/node/protocols/protocol_transaction_out_106.hpp b/include/bitcoin/node/protocols/protocol_transaction_out_106.hpp index 39888a52..4903397e 100644 --- a/include/bitcoin/node/protocols/protocol_transaction_out_106.hpp +++ b/include/bitcoin/node/protocols/protocol_transaction_out_106.hpp @@ -54,7 +54,8 @@ class BCN_API protocol_transaction_out_106 virtual type_id inventory_type() const NOEXCEPT; /// The hash by which the tx is announced to the peer. - virtual system::hash_digest identifier(transaction_t link) const NOEXCEPT; + virtual system::hash_digest tx_identifier( + transaction_t link) const NOEXCEPT; /// The tx of a tx inventory item, terminal if not archived. virtual database::tx_link to_transaction( diff --git a/include/bitcoin/node/protocols/protocol_transaction_out_70016.hpp b/include/bitcoin/node/protocols/protocol_transaction_out_70016.hpp index 80354c18..33d6326b 100644 --- a/include/bitcoin/node/protocols/protocol_transaction_out_70016.hpp +++ b/include/bitcoin/node/protocols/protocol_transaction_out_70016.hpp @@ -42,7 +42,8 @@ class BCN_API protocol_transaction_out_70016 protected: /// Transactions are announced, requested and served by witness hash. type_id inventory_type() const NOEXCEPT override; - system::hash_digest identifier(transaction_t link) const NOEXCEPT override; + system::hash_digest tx_identifier( + transaction_t link) const NOEXCEPT override; database::tx_link to_transaction( const inventory_item& item) const NOEXCEPT override; }; diff --git a/src/chasers/chaser_estimate.cpp b/src/chasers/chaser_estimate.cpp index 098894c9..eb67edc4 100644 --- a/src/chasers/chaser_estimate.cpp +++ b/src/chasers/chaser_estimate.cpp @@ -119,10 +119,17 @@ bool chaser_estimate::handle_chase(const code&, event_value value) NOEXCEPT switch (to_chase(value)) { - // chase::block is only sent when current. This is captured as a cheap - // way to test currency for initialization. Once initialized it is not - // used again. chase::organized is used instead, to ensure that there - // are no push() gaps due to falling out of currency. + // Initialization follows the prune (which rewrites the pool) and + // retries on each current block until the chain reaches the horizon. + case chase::pruned: + { + if (!initialized()) + { + POST(do_pruned, to_payload(value).link); + } + + break; + } case chase::block: { if (!initialized()) @@ -175,11 +182,18 @@ bool chaser_estimate::handle_chase(const code&, event_value value) NOEXCEPT return true; } +void chaser_estimate::do_pruned(header_t link) NOEXCEPT +{ + BC_ASSERT(stranded()); + pruned_ = true; + do_initialize(link); +} + void chaser_estimate::do_initialize(header_t) NOEXCEPT { BC_ASSERT(stranded()); - if (initialized()) + if (!pruned_ || initialized()) return; // Preempt initialize fault when horizon exceeds chain length. diff --git a/src/chasers/chaser_snapshot.cpp b/src/chasers/chaser_snapshot.cpp index ba8bcd5e..cbb52992 100644 --- a/src/chasers/chaser_snapshot.cpp +++ b/src/chasers/chaser_snapshot.cpp @@ -137,7 +137,7 @@ bool chaser_snapshot::handle_chase(const code&, event_value value) NOEXCEPT // events // ---------------------------------------------------------------------------- -void chaser_snapshot::do_prune(header_t) NOEXCEPT +void chaser_snapshot::do_prune(header_t link) NOEXCEPT { BC_ASSERT(stranded()); if (pruned_.load(std::memory_order_relaxed) || @@ -163,6 +163,7 @@ void chaser_snapshot::do_prune(header_t) NOEXCEPT { // The resumption is observable, so the prune is marked before it. pruned_.store(true, std::memory_order_relaxed); + notify(error::success, chases::pruned{ link }); // Could become full before prune start (and it could still succeed). if (running && !archive().is_full()) diff --git a/src/chasers/chaser_transaction.cpp b/src/chasers/chaser_transaction.cpp index 768f3fcc..096b5587 100644 --- a/src/chasers/chaser_transaction.cpp +++ b/src/chasers/chaser_transaction.cpp @@ -310,15 +310,32 @@ database::tx_link chaser_transaction::to_stored( code chaser_transaction::fill(database::tx_link& out, const chain::transaction& tx, const chain::context& ctx) NOEXCEPT { + auto& query = archive(); out = to_stored(tx); if (!out.is_terminal()) + { + if (tx.is_coinbase() || !query.is_pooling() || + query.is_pooled(out) || query.is_confirmed_tx(out)) + return error::success; + + if (const auto ec = validate(tx, ctx)) + return ec; + + if (!query.set_pooled(out, tx, ctx)) + return fault(error::transaction2); + + if (ctx.height >= query.silent_start_height() && + ctx.is_enabled(flags::bip341_rule) && !query.set_silent(out, tx)) + return fault(error::transaction6); + + notify(error::success, chases::transaction{ out }); return error::success; + } if (const auto ec = validate(tx, ctx)) return ec; bool pooled{}; - auto& query = archive(); // Disk full may leave txs partly archived, resolves by resubmit. if (const auto ec = query.set_code(out, pooled, tx)) diff --git a/src/protocols/protocol_transaction_in_106.cpp b/src/protocols/protocol_transaction_in_106.cpp index eeca98b4..f20040df 100644 --- a/src/protocols/protocol_transaction_in_106.cpp +++ b/src/protocols/protocol_transaction_in_106.cpp @@ -64,7 +64,7 @@ type_id protocol_transaction_in_106::get_data_type() const NOEXCEPT return tx_type_; } -hash_digest protocol_transaction_in_106::identifier( +hash_digest protocol_transaction_in_106::tx_identifier( const chain::transaction& tx) const NOEXCEPT { return tx.get_hash(false); @@ -161,9 +161,9 @@ bool protocol_transaction_in_106::handle_receive_transaction(const code& ec, return false; const auto& tx = message->transaction_ptr; - if (!erase_requested(identifier(*tx))) + if (!erase_requested(tx_identifier(*tx))) { - LOGR("Unrequested tx [" << encode_hash(identifier(*tx)) + LOGR("Unrequested tx [" << encode_hash(tx_identifier(*tx)) << "] from [" << opposite() << "]."); stop(network::error::protocol_violation); return false; diff --git a/src/protocols/protocol_transaction_in_70016.cpp b/src/protocols/protocol_transaction_in_70016.cpp index 5fbeb238..72d57081 100644 --- a/src/protocols/protocol_transaction_in_70016.cpp +++ b/src/protocols/protocol_transaction_in_70016.cpp @@ -44,7 +44,7 @@ type_id protocol_transaction_in_70016::get_data_type() const NOEXCEPT return type_id::wtxid; } -hash_digest protocol_transaction_in_70016::identifier( +hash_digest protocol_transaction_in_70016::tx_identifier( const chain::transaction& tx) const NOEXCEPT { return tx.get_hash(true); diff --git a/src/protocols/protocol_transaction_out_106.cpp b/src/protocols/protocol_transaction_out_106.cpp index de47f640..fa837980 100644 --- a/src/protocols/protocol_transaction_out_106.cpp +++ b/src/protocols/protocol_transaction_out_106.cpp @@ -67,7 +67,7 @@ type_id protocol_transaction_out_106::inventory_type() const NOEXCEPT return type_id::transaction; } -hash_digest protocol_transaction_out_106::identifier( +hash_digest protocol_transaction_out_106::tx_identifier( transaction_t link) const NOEXCEPT { return archive().get_tx_key(link); @@ -115,7 +115,7 @@ bool protocol_transaction_out_106::do_announce(transaction_t link) NOEXCEPT if (stopped()) return false; - return announce(identifier(link)); + return announce(tx_identifier(link)); } bool protocol_transaction_out_106::announce(const hash_digest& hash) NOEXCEPT diff --git a/src/protocols/protocol_transaction_out_60002.cpp b/src/protocols/protocol_transaction_out_60002.cpp index 550159a6..2ac4afa9 100644 --- a/src/protocols/protocol_transaction_out_60002.cpp +++ b/src/protocols/protocol_transaction_out_60002.cpp @@ -105,7 +105,7 @@ void protocol_transaction_out_60002::send_memory_pool(const code& ec, if (is_filtered(link)) continue; - if (const auto hash = identifier(link); hash != null_hash) + if (const auto hash = tx_identifier(link); hash != null_hash) items.emplace_back(inventory_type(), hash); } } diff --git a/src/protocols/protocol_transaction_out_70016.cpp b/src/protocols/protocol_transaction_out_70016.cpp index 256cf54f..dca4addf 100644 --- a/src/protocols/protocol_transaction_out_70016.cpp +++ b/src/protocols/protocol_transaction_out_70016.cpp @@ -41,7 +41,7 @@ type_id protocol_transaction_out_70016::inventory_type() const NOEXCEPT return type_id::wtxid; } -hash_digest protocol_transaction_out_70016::identifier( +hash_digest protocol_transaction_out_70016::tx_identifier( transaction_t link) const NOEXCEPT { return archive().get_wtxid(link); diff --git a/test/chasers/chaser_estimate.cpp b/test/chasers/chaser_estimate.cpp index 6b9d12c9..62a6c766 100644 --- a/test/chasers/chaser_estimate.cpp +++ b/test/chasers/chaser_estimate.cpp @@ -80,10 +80,10 @@ struct chaser_estimate_setup_fixture return value; } - void notify_block(const hash_digest& hash) + void notify_pruned(const hash_digest& hash) { const auto link = query_.to_header(hash); - node_.notify(node::error::success, chases::block{ link.value }); + node_.notify(node::error::success, chases::pruned{ link.value }); } void notify_organized(const hash_digest& hash) @@ -184,14 +184,14 @@ BOOST_FIXTURE_TEST_CASE(chaser_estimate__estimate__not_initialized__estimate_pre BOOST_FIXTURE_TEST_CASE(chaser_estimate__estimate__horizon_exceeds_chain__estimate_premature, chaser_estimate_excess_setup_fixture) { - notify_block(p2p_compact_setup_fixture::block1().hash()); + notify_pruned(p2p_compact_setup_fixture::block1().hash()); const auto result = estimate(1, estimator::mode::basic); BOOST_REQUIRE_EQUAL(result.first, node::error::estimate_premature); } BOOST_FIXTURE_TEST_CASE(chaser_estimate__estimate__initialized_no_fees__estimate_false, chaser_estimate_enabled_setup_fixture) { - notify_block(p2p_compact_setup_fixture::block1().hash()); + notify_pruned(p2p_compact_setup_fixture::block1().hash()); const auto result = initialized(1, estimator::mode::basic); BOOST_REQUIRE_EQUAL(result.first, node::error::estimate_false); BOOST_REQUIRE_EQUAL(result.second, estimator::estimate_failed); @@ -201,10 +201,10 @@ BOOST_FIXTURE_TEST_CASE(chaser_estimate__estimate__initialized_no_fees__estimate ////{ //// const auto& block1 = p2p_compact_setup_fixture::block1(); //// const auto& block2 = p2p_compact_setup_fixture::block2(); -//// notify_block(block1.hash()); +//// notify_pruned(block1.hash()); //// BOOST_REQUIRE_EQUAL(initialized(1, estimator::mode::basic).first, node::error::estimate_false); -//// notify_block(block1.hash()); +//// notify_pruned(block1.hash()); //// notify_organized(block1.hash()); //// BOOST_REQUIRE(confirm2()); //// notify_organized(block2.hash()); @@ -222,7 +222,7 @@ BOOST_FIXTURE_TEST_CASE(chaser_estimate__top_height__not_initialized__zero, chas ////BOOST_FIXTURE_TEST_CASE(chaser_estimate__organized__invalid_link__suspended, chaser_estimate_enabled_setup_fixture) ////{ -//// notify_block(p2p_compact_setup_fixture::block1().hash()); +//// notify_pruned(p2p_compact_setup_fixture::block1().hash()); //// BOOST_REQUIRE_EQUAL(initialized(1, estimator::mode::basic).first, node::error::estimate_false); //// node_.notify(node::error::success, chases::organized{ node::header_t{ 42 } }); @@ -231,7 +231,7 @@ BOOST_FIXTURE_TEST_CASE(chaser_estimate__top_height__not_initialized__zero, chas ////BOOST_FIXTURE_TEST_CASE(chaser_estimate__organized__unconfirmed_above_top__suspended, chaser_estimate_enabled_setup_fixture) ////{ -//// notify_block(p2p_compact_setup_fixture::block1().hash()); +//// notify_pruned(p2p_compact_setup_fixture::block1().hash()); //// BOOST_REQUIRE_EQUAL(initialized(1, estimator::mode::basic).first, node::error::estimate_false); //// BOOST_REQUIRE(store_header2()); @@ -241,7 +241,7 @@ BOOST_FIXTURE_TEST_CASE(chaser_estimate__top_height__not_initialized__zero, chas ////BOOST_FIXTURE_TEST_CASE(chaser_estimate__reorganized__invalid_link__suspended, chaser_estimate_enabled_setup_fixture) ////{ -//// notify_block(p2p_compact_setup_fixture::block1().hash()); +//// notify_pruned(p2p_compact_setup_fixture::block1().hash()); //// BOOST_REQUIRE_EQUAL(initialized(1, estimator::mode::basic).first, node::error::estimate_false); //// node_.notify(node::error::success, chases::reorganized{ node::header_t{ 42 } }); diff --git a/test/chasers/chaser_transaction.cpp b/test/chasers/chaser_transaction.cpp index d6799afc..559e7505 100644 --- a/test/chasers/chaser_transaction.cpp +++ b/test/chasers/chaser_transaction.cpp @@ -521,6 +521,22 @@ BOOST_FIXTURE_TEST_CASE(chaser_transaction__submit_compact__invalid_tx_link__int BOOST_REQUIRE(!query_.is_associated(link)); } +BOOST_FIXTURE_TEST_CASE(chaser_transaction__submit_compact__stored_unpooled_tx__pooled, chaser_transaction_header_setup_fixture) +{ + const auto link = query_.to_header(p2p_compact_setup_fixture::block1().hash()); + const auto tx = spend(parent_value); + BOOST_REQUIRE(query_.set(*tx)); + + const auto stored = query_.to_tx(tx->hash(false)); + BOOST_REQUIRE(!query_.is_pooled(stored)); + + const auto result = submit_compact(package({ tx }), { database::tx_link{ 0 }, database::tx_link::terminal }, link); + BOOST_REQUIRE_EQUAL(result.first, node::error::success); + BOOST_REQUIRE(query_.is_associated(link)); + BOOST_REQUIRE(query_.is_pooled(stored)); + BOOST_REQUIRE_EQUAL(query_.to_tx(tx->hash(false)), stored); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(protocol_transaction_tests) diff --git a/test/full_node.cpp b/test/full_node.cpp index a4e80fef..2bac596f 100644 --- a/test/full_node.cpp +++ b/test/full_node.cpp @@ -352,6 +352,17 @@ BOOST_AUTO_TEST_CASE(full_node__notify__snap__resumed) BOOST_REQUIRE_EQUAL(future.get(), error::success); } +BOOST_AUTO_TEST_CASE(full_node__notify__block__pruned) +{ + BOOST_REQUIRE(initialize()); + BOOST_REQUIRE(!start()); + BOOST_REQUIRE(!run()); + auto pruned = subscribe(chase::pruned); + node_.notify(error::success, chases::block{ 0 }); + BOOST_REQUIRE(ready(pruned)); + BOOST_REQUIRE_EQUAL(pruned.get(), error::success); +} + BOOST_AUTO_TEST_CASE(full_node__notify__block_after_prune__not_suspended) { BOOST_REQUIRE(initialize()); diff --git a/test/functional/p2p_compact.cpp b/test/functional/p2p_compact.cpp index 7019389e..b850da98 100644 --- a/test/functional/p2p_compact.cpp +++ b/test/functional/p2p_compact.cpp @@ -189,7 +189,8 @@ BOOST_AUTO_TEST_CASE(functional_p2p_compact__compact_block__replayed_while_organ BOOST_REQUIRE(!received(get_compact_transactions::command, pong::command)); } -BOOST_AUTO_TEST_CASE(functional_p2p_compact__compact_block__replayed_while_submitting__not_requested) +// Block 2 remains an unassociated candidate, so the chain never coalesces. +BOOST_FIXTURE_TEST_CASE(functional_p2p_compact__compact_block__replayed_while_submitting__not_requested, p2p_compact_candidate_setup_fixture) { BOOST_REQUIRE(handshake(full_node));