Skip to content

Commit

Permalink
Incrementally update sv2 block template
Browse files Browse the repository at this point in the history
  • Loading branch information
Sjors committed Aug 29, 2024
1 parent 54eaa65 commit fa63eb0
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 15 deletions.
118 changes: 108 additions & 10 deletions src/node/sv2_template_provider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ bool Sv2TemplateProvider::Start(const Sv2TemplateProviderOptions& options)
}

m_thread_sv2_handler = std::thread(&util::TraceThread, "sv2", [this] { ThreadSv2Handler(); });
m_thread_sv2_mempool_handler = std::thread(&util::TraceThread, "sv2mempool", [this] { ThreadSv2MempoolHandler(); });
return true;
}

Expand All @@ -73,6 +74,9 @@ void Sv2TemplateProvider::StopThreads()
if (m_thread_sv2_handler.joinable()) {
m_thread_sv2_handler.join();
}
if (m_thread_sv2_mempool_handler.joinable()) {
m_thread_sv2_mempool_handler.join();
}
}

void Sv2TemplateProvider::ThreadSv2Handler()
Expand All @@ -94,21 +98,21 @@ void Sv2TemplateProvider::ThreadSv2Handler()
}

if (best_block_changed) {
LOCK(m_tp_mutex);
m_best_prev_hash = tip.hash;
m_last_block_time = GetTime<std::chrono::seconds>();
}
{
LOCK(m_tp_mutex);
m_best_prev_hash = tip.hash;
m_last_block_time = GetTime<std::chrono::seconds>();
m_template_last_update = GetTime<std::chrono::seconds>();
}

// In a later commit we'll also push new templates based on changes to
// the mempool, so this if condition will no longer match the one above.
if (best_block_changed) {
m_connman->ForEachClient([this, best_block_changed](Sv2Client& client) {
// For newly connected clients, we call SendWork after receiving
// CoinbaseOutputDataSize.
if (client.m_coinbase_tx_outputs_size == 0) return;

LOCK(this->m_tp_mutex);
if (!SendWork(client, /*send_new_prevhash=*/best_block_changed)) {
CAmount dummy_last_fees;
if (!SendWork(client, /*send_new_prevhash=*/best_block_changed, dummy_last_fees)) {
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Disconnecting client id=%zu\n",
client.m_id);
client.m_disconnect_flag = true;
Expand All @@ -121,13 +125,96 @@ void Sv2TemplateProvider::ThreadSv2Handler()
}
}

class Timer {
private:
std::chrono::seconds m_interval;
std::chrono::seconds m_last_triggered;

public:
Timer(std::chrono::seconds interval) : m_interval(interval) {
reset();
}

bool trigger() {
auto now{GetTime<std::chrono::seconds>()};
if (now - m_last_triggered >= m_interval) {
m_last_triggered = now;
return true;
}
return false;
}

void reset() {
auto now{GetTime<std::chrono::seconds>()};
m_last_triggered = now;
}
};

void Sv2TemplateProvider::ThreadSv2MempoolHandler()
{
Timer timer(m_options.fee_check_interval);

//! Fees for the previous fee_check_interval
CAmount fees_previous_interval{0};
//! Fees as of the last waitFeesChanged() call
CAmount last_fees{0};

while (!m_flag_interrupt_sv2) {
auto timeout{std::min(std::chrono::milliseconds(100), std::chrono::milliseconds(m_options.fee_check_interval))};
last_fees = fees_previous_interval;
bool tip_changed{false};
if (!m_mining.waitFeesChanged(timeout, WITH_LOCK(m_tp_mutex, return m_best_prev_hash;), m_options.fee_delta, last_fees, tip_changed)) {
if (tip_changed) {
timer.reset();
fees_previous_interval = 0;
last_fees = 0;
}
continue;
}

// Do not send new templates more frequently than the fee check interval
if (!timer.trigger()) continue;

// If we never created a template, continue
if (m_template_last_update == std::chrono::milliseconds(0)) continue;

// TODO ensure all connected clients have had work queued up for the latest prevhash.

// This doesn't have any effect, but it will once waitFeesChanged() updates the last_fees value.
fees_previous_interval = last_fees;

m_connman->ForEachClient([this, last_fees, &fees_previous_interval](Sv2Client& client) {
// For newly connected clients, we call SendWork after receiving
// CoinbaseOutputDataSize.
if (client.m_coinbase_tx_outputs_size == 0) return;

LOCK(this->m_tp_mutex);
// fees_previous_interval is only updated if the fee increase was sufficient,
// since waitFeesChanged doesn't actually check this yet.

CAmount fees_before = last_fees;
if (!SendWork(client, /*send_new_prevhash=*/false, fees_before)) {
LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Disconnecting client id=%zu\n",
client.m_id);
client.m_disconnect_flag = true;
}

// We don't track fees_before for individual connected clients. Pick the
// highest value amongst all connected clients (which may vary in additional_coinbase_weight).
if (fees_before > fees_previous_interval) fees_previous_interval = fees_before;
});
}
}


void Sv2TemplateProvider::ReceivedMessage(Sv2Client& client, node::Sv2MsgType msg_type) {
switch (msg_type)
{
case node::Sv2MsgType::COINBASE_OUTPUT_DATA_SIZE:
{
LOCK(m_tp_mutex);
if (!SendWork(client, /*send_new_prevhash=*/true)) {
CAmount dummy_last_fees;
if (!SendWork(client, /*send_new_prevhash=*/true, dummy_last_fees)) {
return;
}
break;
Expand Down Expand Up @@ -240,7 +327,7 @@ void Sv2TemplateProvider::PruneBlockTemplateCache()
});
}

bool Sv2TemplateProvider::SendWork(Sv2Client& client, bool send_new_prevhash)
bool Sv2TemplateProvider::SendWork(Sv2Client& client, bool send_new_prevhash, CAmount& fees_before)
{
AssertLockHeld(m_tp_mutex);

Expand All @@ -264,6 +351,17 @@ bool Sv2TemplateProvider::SendWork(Sv2Client& client, bool send_new_prevhash)
m_best_prev_hash = new_work_set.block_template->getBlockHeader().hashPrevBlock;
}

// Do not submit new template if the fee increase is insufficient.
// TODO: drop this when waitFeesChanged actually checks fee_delta.
CAmount fees = 0;
for (CAmount fee : new_work_set.block_template->getTxFees()) {
// Skip coinbase
if (fee < 0) continue;
fees += fee;
}
if (!send_new_prevhash && fees_before + m_options.fee_delta > fees) return true;
fees_before = fees;

LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Send 0x71 NewTemplate id=%lu to client id=%zu\n", m_template_id, client.m_id);
client.m_send_messages.emplace_back(new_work_set.new_template);

Expand Down
32 changes: 31 additions & 1 deletion src/node/sv2_template_provider.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ struct Sv2TemplateProviderOptions
* The listening port for the server.
*/
uint16_t port{8336};

/**
* Minimum fee delta to send new template upstream
*/
CAmount fee_delta{1000};

/**
* Block template update interval (to check for increased fees)
*/
std::chrono::seconds fee_check_interval{30};
};

/**
Expand Down Expand Up @@ -59,6 +69,11 @@ class Sv2TemplateProvider : public Sv2EventsInterface
*/
std::thread m_thread_sv2_handler;

/**
* The secondary thread for the template provider.
*/
std::thread m_thread_sv2_mempool_handler;

/**
* Signal for handling interrupts and stopping the template provider event loop.
*/
Expand All @@ -71,6 +86,12 @@ class Sv2TemplateProvider : public Sv2EventsInterface
*/
uint64_t m_template_id GUARDED_BY(m_tp_mutex){0};

/**
* Last time we created a new template
*/
std::chrono::milliseconds m_template_last_update{0};


/**
* The current best known block hash in the network.
*/
Expand Down Expand Up @@ -106,6 +127,13 @@ class Sv2TemplateProvider : public Sv2EventsInterface
*/
void ThreadSv2Handler() EXCLUSIVE_LOCKS_REQUIRED(!m_tp_mutex);

/**
* Secondary thread for the template provider, contains an event loop handling
* mempool updates.
*/
void ThreadSv2MempoolHandler() EXCLUSIVE_LOCKS_REQUIRED(!m_tp_mutex);


/**
* Triggered on interrupt signals to stop the main event loop in ThreadSv2Handler().
*/
Expand Down Expand Up @@ -155,8 +183,10 @@ class Sv2TemplateProvider : public Sv2EventsInterface

/**
* Sends the best NewTemplate and SetNewPrevHash to a client.
*
* TODO: drop fees_before argument after cluster mempool
*/
[[nodiscard]] bool SendWork(Sv2Client& client, bool send_new_prevhash) EXCLUSIVE_LOCKS_REQUIRED(m_tp_mutex);
[[nodiscard]] bool SendWork(Sv2Client& client, bool send_new_prevhash, CAmount& fees_before) EXCLUSIVE_LOCKS_REQUIRED(m_tp_mutex);

};

Expand Down
86 changes: 82 additions & 4 deletions src/test/sv2_template_provider_tests.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#include <addresstype.h>
#include <boost/test/unit_test.hpp>
#include <common/sv2_messages.h>
#include <interfaces/mining.h>
#include <node/sv2_template_provider.h>
#include <node/transaction.h>
#include <test/util/net.h>
#include <test/util/setup_common.h>
#include <test/util/transaction_utils.h>
#include <util/sock.h>
#include <util/strencodings.h>

#include <memory>

Expand Down Expand Up @@ -163,7 +167,43 @@ BOOST_AUTO_TEST_CASE(client_tests)
// There should now be one template
BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 1);

// Get the template id
// Move mock time by at least the fee check interval
// If the mempool doesn't change, no new template is generated.
SetMockTime(GetMockTime() + std::chrono::seconds{tester.m_tp_options.fee_check_interval});
// Briefly wait for the timer in ThreadSv2Handler and block creation
UninterruptibleSleep(std::chrono::milliseconds{200});
BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 1);

// Create a transaction with a large fee
size_t tx_size;
CKey key = GenerateRandomKey();
CScript locking_script = GetScriptForDestination(PKHash(key.GetPubKey()));
// Don't hold on to the transaction
{
LOCK(cs_main);
BOOST_REQUIRE_EQUAL(m_node.mempool->size(), 0);

auto mtx = CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[0], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/locking_script,
/*output_amount=*/CAmount(49 * COIN), /*submit=*/true);
CTransactionRef tx = MakeTransactionRef(mtx);

// Get serialized transaction size
DataStream ss;
ss << TX_WITH_WITNESS(tx);
tx_size = ss.size();

BOOST_REQUIRE_EQUAL(m_node.mempool->size(), 1);
}

// Move mock time
SetMockTime(GetMockTime() + std::chrono::seconds{tester.m_tp_options.fee_check_interval});

// Briefly wait for the timer in ThreadSv2MempoolHandler and block creation
UninterruptibleSleep(std::chrono::milliseconds{200});

// Get the latest template id
uint64_t template_id = 0;
{
LOCK(tester.m_tp->m_tp_mutex);
Expand All @@ -174,6 +214,11 @@ BOOST_AUTO_TEST_CASE(client_tests)
}
}

// Expect our peer to receive a NewTemplate message
// This time it should contain the 32 byte prevhash (unchanged)
constexpr size_t expected_len = SV2_HEADER_ENCRYPTED_SIZE + 91 + 32 + Poly1305::TAGLEN;
BOOST_REQUIRE_EQUAL(tester.PeerReceiveBytes(), expected_len);

// Have the peer send us RequestTransactionData
// We should reply with RequestTransactionData.Success
node::Sv2NetHeader req_tx_data_header{node::Sv2MsgType::REQUEST_TRANSACTION_DATA, 8};
Expand All @@ -187,15 +232,48 @@ BOOST_AUTO_TEST_CASE(client_tests)
tester.receiveMessage(msg);
const size_t template_id_size = 8;
const size_t excess_data_size = 2 + 32;
size_t tx_list_size = 2; // no transactions, so transaction_list is 0x0100
size_t tx_list_size = 2 + 3 + tx_size;
BOOST_REQUIRE_EQUAL(tester.PeerReceiveBytes(), SV2_HEADER_ENCRYPTED_SIZE + template_id_size + excess_data_size + tx_list_size + Poly1305::TAGLEN);
{
LOCK(cs_main);

// RBF the transaction with with > DEFAULT_SV2_FEE_DELTA
CreateValidMempoolTransaction(/*input_transaction=*/m_coinbase_txns[0], /*input_vout=*/0,
/*input_height=*/0, /*input_signing_key=*/coinbaseKey,
/*output_destination=*/locking_script,
/*output_amount=*/CAmount(48 * COIN), /*submit=*/true);

BOOST_REQUIRE_EQUAL(m_node.mempool->size(), 1);
}

// Move mock time
SetMockTime(GetMockTime() + std::chrono::seconds{tester.m_tp_options.fee_check_interval});

// Briefly wait for the timer in ThreadSv2Handler and block creation
UninterruptibleSleep(std::chrono::milliseconds{200});

// Check that there's a new template
BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 3);

// Wait a bit more for macOS native CI
UninterruptibleSleep(std::chrono::milliseconds{1000});

// Expect our peer to receive a NewTemplate message
BOOST_REQUIRE_EQUAL(tester.PeerReceiveBytes(), SV2_HEADER_ENCRYPTED_SIZE + 91 + 32 + Poly1305::TAGLEN);

// Have the peer send us RequestTransactionData for the old template
// We should reply with RequestTransactionData.Success, and the original
// (replaced) transaction
tester.receiveMessage(msg);
tx_list_size = 2 + 3 + tx_size;
BOOST_REQUIRE_EQUAL(tester.PeerReceiveBytes(), SV2_HEADER_ENCRYPTED_SIZE + template_id_size + excess_data_size + tx_list_size + Poly1305::TAGLEN);

// Create a new block
mineBlocks(1);

// We should send out another NewTemplate and SetNewPrevHash
// The new template contains the new prevhash.
BOOST_REQUIRE_EQUAL(tester.PeerReceiveBytes(), 2 * SV2_HEADER_ENCRYPTED_SIZE + 91 + 80 + 2 * Poly1305::TAGLEN);
BOOST_REQUIRE_EQUAL(tester.PeerReceiveBytes(), 2 * SV2_HEADER_ENCRYPTED_SIZE + 91 + 32 + 80 + 2 * Poly1305::TAGLEN);
// The SetNewPrevHash message is redundant
// TODO: don't send it?
// Background: in the future we want to send an empty or optimistic template
Expand All @@ -204,7 +282,7 @@ BOOST_AUTO_TEST_CASE(client_tests)
// a new block, and construct a better template _after_ that.

// Templates are briefly preserved
BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 2);
BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 4);

// Do not provide transactions for stale templates
// TODO
Expand Down

0 comments on commit fa63eb0

Please sign in to comment.