diff --git a/src/sv2/template_provider.cpp b/src/sv2/template_provider.cpp index 31705dc5..2b792253 100644 --- a/src/sv2/template_provider.cpp +++ b/src/sv2/template_provider.cpp @@ -332,7 +332,6 @@ void Sv2TemplateProvider::ThreadSv2ClientHandler(size_t client_id) // -sv2interval=N requires that we don't send fee updates until at least // N seconds have gone by. So we first call waitNext() without a fee // threshold, and then on the next while iteration we set it. - // TODO: add test coverage const bool check_fees{m_options.is_test || timer.trigger()}; CAmount fee_delta{check_fees ? m_options.fee_delta : MAX_MONEY}; diff --git a/src/test/sv2_template_provider_tests.cpp b/src/test/sv2_template_provider_tests.cpp index 698285eb..8579e6e7 100644 --- a/src/test/sv2_template_provider_tests.cpp +++ b/src/test/sv2_template_provider_tests.cpp @@ -44,80 +44,18 @@ BOOST_AUTO_TEST_CASE(client_tests) tester.handshake(); - // After the handshake the client must send a SetupConnection message to the - // Template Provider. - - tester.handshake(); BOOST_TEST_MESSAGE("Handshake done, send SetupConnectionMsg"); - - node::Sv2NetMsg setup{tester.SetupConnectionMsg()}; - tester.receiveMessage(setup); - // SetupConnection.Success is 6 bytes - BOOST_REQUIRE_EQUAL(tester.PeerReceiveBytes(), SV2_HEADER_ENCRYPTED_SIZE + 6 + Poly1305::TAGLEN); + tester.SendSetupConnection(); // There should be no block templates before any client gave us their coinbase // output data size: BOOST_REQUIRE(tester.GetBlockTemplateCount() == 0); - std::vector coinbase_output_constraint_bytes{ - 0x01, 0x00, 0x00, 0x00, // coinbase_output_max_additional_size - 0x00, 0x00 // coinbase_output_max_sigops - }; - node::Sv2NetMsg msg{node::Sv2MsgType::COINBASE_OUTPUT_CONSTRAINTS, std::move(coinbase_output_constraint_bytes)}; - tester.receiveMessage(msg); + tester.SendCoinbaseOutputConstraints(); BOOST_TEST_MESSAGE("The reply should be NewTemplate and SetNewPrevHash"); - // Payload sizes for fixed-layout SV2 messages used in this test - constexpr size_t SV2_SET_NEW_PREV_HASH_MESSAGE_SIZE = 8 + 32 + 4 + 4 + 32; // = 80 - constexpr size_t SV2_NEW_TEMPLATE_MESSAGE_SIZE = - 8 + // template_id - 1 + // future_template - 4 + // version - 4 + // coinbase_tx_version - 2 + // coinbase_prefix (CompactSize(1) + 1-byte OP_0) - 4 + // coinbase_tx_input_sequence - 8 + // coinbase_tx_value_remaining - 4 + // coinbase_tx_outputs_count (2 - mock creates 3, only 2 OP_RETURN outputs pass filter) - 2 + 56 + // B0_64K: length prefix (2 bytes) + 2 outputs (witness commitment 43 bytes + merge mining 13 bytes) - 4 + // coinbase_tx_locktime - 1; // merkle_path count (CompactSize(0)) - - // Two messages (SetNewPrevHash + NewTemplate) may arrive in one read or sequentially. - const size_t expected_set_new_prev_hash = SV2_HEADER_ENCRYPTED_SIZE + SV2_SET_NEW_PREV_HASH_MESSAGE_SIZE + Poly1305::TAGLEN; - const size_t expected_new_template = SV2_HEADER_ENCRYPTED_SIZE + SV2_NEW_TEMPLATE_MESSAGE_SIZE + Poly1305::TAGLEN; - const size_t expected_pair_bytes = expected_set_new_prev_hash + expected_new_template; - - const auto expect_template_pair = [&](const char* context) { - size_t accumulated = 0; - bool seen_prev_hash = false; - bool seen_new_template = false; - int iterations = 0; - - while (accumulated < expected_pair_bytes) { - size_t chunk = tester.PeerReceiveBytes(); - accumulated += chunk; - ++iterations; - - if (chunk == expected_set_new_prev_hash) { - seen_prev_hash = true; - } else if (chunk == expected_new_template) { - seen_new_template = true; - } else if (chunk == expected_pair_bytes) { - seen_prev_hash = true; - seen_new_template = true; - break; - } else { - BOOST_FAIL(std::string("Unexpected message size while receiving ") + context); - } - - BOOST_REQUIRE_MESSAGE(iterations <= 2, std::string("Too many fragments for ") + context); - } + tester.ReceiveTemplatePair(); - BOOST_REQUIRE_MESSAGE(seen_prev_hash, std::string("Missing SetNewPrevHash during ") + context); - BOOST_REQUIRE_MESSAGE(seen_new_template, std::string("Missing NewTemplate during ") + context); - BOOST_REQUIRE_MESSAGE(accumulated == expected_pair_bytes, std::string("Incomplete response for ") + context); - }; - - expect_template_pair("initial template broadcast"); + const size_t expected_new_template = SV2_HEADER_ENCRYPTED_SIZE + TPTester::SV2_NEW_TEMPLATE_MSG_SIZE + Poly1305::TAGLEN; // There should now be one template BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 1); @@ -144,7 +82,7 @@ BOOST_AUTO_TEST_CASE(client_tests) BOOST_TEST_MESSAGE("Receive NewTemplate (fee increase)"); // One NewTemplate follows: header + payload + payload tag size_t bytes_fee_nt = tester.PeerReceiveBytes(); - BOOST_REQUIRE_EQUAL(bytes_fee_nt, SV2_HEADER_ENCRYPTED_SIZE + SV2_NEW_TEMPLATE_MESSAGE_SIZE + Poly1305::TAGLEN); + BOOST_REQUIRE_EQUAL(bytes_fee_nt, expected_new_template); // Get the latest template id uint64_t template_id = 0; @@ -203,7 +141,7 @@ BOOST_AUTO_TEST_CASE(client_tests) // Expect our peer to receive a NewTemplate message size_t bytes_second_nt = tester.PeerReceiveBytes(); - BOOST_REQUIRE_EQUAL(bytes_second_nt, SV2_HEADER_ENCRYPTED_SIZE + SV2_NEW_TEMPLATE_MESSAGE_SIZE + Poly1305::TAGLEN); + BOOST_REQUIRE_EQUAL(bytes_second_nt, expected_new_template); // Check that there's a new template BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 3); @@ -234,7 +172,7 @@ BOOST_AUTO_TEST_CASE(client_tests) BOOST_REQUIRE(tester.m_mining_control->WaitForTemplateSeq(seq_after_second_nt + 1)); // We should send out another NewTemplate and SetNewPrevHash (two messages) - expect_template_pair("new tip template broadcast"); + tester.ReceiveTemplatePair(); // The SetNewPrevHash message is redundant // TODO: don't send it? // Background: in the future we want to send an empty or optimistic template @@ -268,4 +206,49 @@ BOOST_AUTO_TEST_CASE(client_tests) tester.m_mining_control->Shutdown(); } +// Test fee-based rate limiting timer (-sv2interval flag). +// Uses is_test=false to exercise actual timer logic. +BOOST_AUTO_TEST_CASE(fee_timer_blocking_test) +{ + // Use real wall-clock time instead of mock time + SetMockTime(std::chrono::seconds{0}); + + Sv2TemplateProviderOptions opts; + opts.is_test = false; + opts.fee_check_interval = std::chrono::seconds{2}; + TPTester tester{opts}; + + tester.handshake(); + tester.SendSetupConnection(); + tester.SendCoinbaseOutputConstraints(); + tester.ReceiveTemplatePair(); + + const size_t expected_new_template = SV2_HEADER_ENCRYPTED_SIZE + TPTester::SV2_NEW_TEMPLATE_MSG_SIZE + Poly1305::TAGLEN; + + uint64_t seq = tester.m_mining_control->GetTemplateSeq(); + + // Trigger a fee increase immediately after template; timer should block it + BOOST_TEST_MESSAGE("Trigger fee increase while timer is blocking"); + std::vector blocked_fee_txs{MakeDummyTx()}; + tester.m_mining_control->TriggerFeeIncrease(blocked_fee_txs); + + bool got_template = tester.m_mining_control->WaitForTemplateSeq(seq + 1, std::chrono::milliseconds{2500}); + BOOST_REQUIRE_MESSAGE(!got_template, "Fee increase should be blocked when timer hasn't fired"); + BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 1); + + // After fee_check_interval (2s), the timer should allow fee checks + BOOST_TEST_MESSAGE("Trigger fee increase after timer fires"); + std::vector allowed_fee_txs{MakeDummyTx()}; + tester.m_mining_control->TriggerFeeIncrease(allowed_fee_txs); + + got_template = tester.m_mining_control->WaitForTemplateSeq(seq + 1, std::chrono::milliseconds{3000}); + BOOST_REQUIRE_MESSAGE(got_template, "Fee increase should be allowed after timer fires"); + + size_t bytes_nt = tester.PeerReceiveBytes(); + BOOST_REQUIRE_EQUAL(bytes_nt, expected_new_template); + BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 2); + + tester.m_mining_control->Shutdown(); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/sv2_tp_tester.cpp b/src/test/sv2_tp_tester.cpp index a641eaba..777bdf74 100644 --- a/src/test/sv2_tp_tester.cpp +++ b/src/test/sv2_tp_tester.cpp @@ -36,8 +36,10 @@ struct MockInit : public interfaces::Init { }; } // namespace -TPTester::TPTester() - : m_state{std::make_shared()}, m_mining_control{std::make_shared(m_state)} +TPTester::TPTester() : TPTester(Sv2TemplateProviderOptions{.is_test = true}) {} + +TPTester::TPTester(Sv2TemplateProviderOptions opts) + : m_tp_options{opts}, m_state{std::make_shared()}, m_mining_control{std::make_shared(m_state)} { // Start cap'n proto event loop on a background thread std::promise loop_ready; @@ -209,3 +211,58 @@ size_t TPTester::GetBlockTemplateCount() LOCK(m_tp->m_tp_mutex); return m_tp->GetBlockTemplates().size(); } + +void TPTester::SendSetupConnection() +{ + node::Sv2NetMsg setup{SetupConnectionMsg()}; + receiveMessage(setup); + // SetupConnection.Success is 6 bytes + BOOST_REQUIRE_EQUAL(PeerReceiveBytes(), SV2_HEADER_ENCRYPTED_SIZE + 6 + Poly1305::TAGLEN); +} + +void TPTester::SendCoinbaseOutputConstraints() +{ + std::vector coinbase_output_constraint_bytes{ + 0x01, 0x00, 0x00, 0x00, // coinbase_output_max_additional_size + 0x00, 0x00 // coinbase_output_max_sigops + }; + node::Sv2NetMsg coc_msg{node::Sv2MsgType::COINBASE_OUTPUT_CONSTRAINTS, std::move(coinbase_output_constraint_bytes)}; + receiveMessage(coc_msg); +} + +size_t TPTester::ReceiveTemplatePair() +{ + const size_t expected_set_new_prev_hash = SV2_HEADER_ENCRYPTED_SIZE + SV2_SET_NEW_PREV_HASH_MSG_SIZE + Poly1305::TAGLEN; + const size_t expected_new_template = SV2_HEADER_ENCRYPTED_SIZE + SV2_NEW_TEMPLATE_MSG_SIZE + Poly1305::TAGLEN; + const size_t expected_pair_bytes = expected_set_new_prev_hash + expected_new_template; + + size_t accumulated = 0; + bool seen_prev_hash = false; + bool seen_new_template = false; + int iterations = 0; + + while (accumulated < expected_pair_bytes) { + size_t chunk = PeerReceiveBytes(); + accumulated += chunk; + ++iterations; + + if (chunk == expected_set_new_prev_hash) { + seen_prev_hash = true; + } else if (chunk == expected_new_template) { + seen_new_template = true; + } else if (chunk == expected_pair_bytes) { + seen_prev_hash = true; + seen_new_template = true; + break; + } else { + BOOST_FAIL("Unexpected message size in template pair"); + } + + BOOST_REQUIRE_MESSAGE(iterations <= 2, "Too many fragments in template pair"); + } + + BOOST_REQUIRE_MESSAGE(seen_prev_hash, "Missing SetNewPrevHash in template pair"); + BOOST_REQUIRE_MESSAGE(seen_new_template, "Missing NewTemplate in template pair"); + BOOST_REQUIRE_EQUAL(accumulated, expected_pair_bytes); + return accumulated; +} diff --git a/src/test/sv2_tp_tester.h b/src/test/sv2_tp_tester.h index 29a12a5d..5082ff99 100644 --- a/src/test/sv2_tp_tester.h +++ b/src/test/sv2_tp_tester.h @@ -42,6 +42,7 @@ class TPTester { std::unique_ptr m_mining_proxy; // IPC mining proxy TPTester(); + explicit TPTester(Sv2TemplateProviderOptions opts); ~TPTester(); void SendPeerBytes(); @@ -50,6 +51,33 @@ class TPTester { void receiveMessage(Sv2NetMsg& msg); Sv2NetMsg SetupConnectionMsg(); size_t GetBlockTemplateCount(); + + /** Send SetupConnection and verify Success reply. */ + void SendSetupConnection(); + /** Send CoinbaseOutputConstraints message. */ + void SendCoinbaseOutputConstraints(); + /** Receive a NewTemplate + SetNewPrevHash pair and verify sizes. Returns total bytes. */ + size_t ReceiveTemplatePair(); + + // SV2 message payload sizes used for test verification + static constexpr size_t SV2_SET_NEW_PREV_HASH_MSG_SIZE = + 8 + // template_id + 32 + // prev_hash + 4 + // header_timestamp + 4 + // nBits + 32; // target + static constexpr size_t SV2_NEW_TEMPLATE_MSG_SIZE = + 8 + // template_id + 1 + // future_template + 4 + // version + 4 + // coinbase_tx_version + 2 + // coinbase_prefix (CompactSize(1) + 1-byte OP_0) + 4 + // coinbase_tx_input_sequence + 8 + // coinbase_tx_value_remaining + 4 + // coinbase_tx_outputs_count + 2 + 56 + // B0_64K: length prefix (2 bytes) + 2 outputs (witness commitment 43 bytes + merge mining 13 bytes) + 4 + // coinbase_tx_locktime + 1; // merkle_path count (CompactSize(0)) }; #endif // BITCOIN_TEST_SV2_TP_TESTER_H