From 668269e6eff0c27665a3d044c444ae2b352c9b99 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 15 Nov 2024 15:42:41 +0100 Subject: [PATCH 1/9] Stratum v2 template provider scaffold The template provider will listen for a Job Declarator client. It can establish a connection and detect various protocol errors. Co-Authored-By: Christopher Coverdale Co-Authored-By: Fi3 --- src/node/context.h | 1 + src/sv2/CMakeLists.txt | 1 + src/sv2/template_provider.cpp | 84 +++++++++++++ src/sv2/template_provider.h | 106 ++++++++++++++++ src/test/CMakeLists.txt | 1 + src/test/sv2_template_provider_tests.cpp | 154 +++++++++++++++++++++++ 6 files changed, 347 insertions(+) create mode 100644 src/sv2/template_provider.cpp create mode 100644 src/sv2/template_provider.h create mode 100644 src/test/sv2_template_provider_tests.cpp diff --git a/src/node/context.h b/src/node/context.h index debc12212064c..8334a84d5b8ac 100644 --- a/src/node/context.h +++ b/src/node/context.h @@ -25,6 +25,7 @@ class ChainstateManager; class ECC_Context; class NetGroupManager; class PeerManager; +class Sv2TemplateProvider; namespace interfaces { class Chain; class ChainClient; diff --git a/src/sv2/CMakeLists.txt b/src/sv2/CMakeLists.txt index a628204612fcd..7b632032c233f 100644 --- a/src/sv2/CMakeLists.txt +++ b/src/sv2/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(bitcoin_sv2 STATIC EXCLUDE_FROM_ALL noise.cpp transport.cpp connman.cpp + template_provider.cpp ) target_link_libraries(bitcoin_sv2 diff --git a/src/sv2/template_provider.cpp b/src/sv2/template_provider.cpp new file mode 100644 index 0000000000000..89a6f38ca1ab9 --- /dev/null +++ b/src/sv2/template_provider.cpp @@ -0,0 +1,84 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +Sv2TemplateProvider::Sv2TemplateProvider(interfaces::Mining& mining) : m_mining{mining} +{ + // TODO: persist static key + CKey static_key; + static_key.MakeNewKey(true); + + auto authority_key{GenerateRandomKey()}; + + // SRI uses base58 encoded x-only pubkeys in its configuration files + std::array version_pubkey_bytes; + version_pubkey_bytes[0] = 1; + version_pubkey_bytes[1] = 0; + m_authority_pubkey = XOnlyPubKey(authority_key.GetPubKey()); + std::copy(m_authority_pubkey.begin(), m_authority_pubkey.end(), version_pubkey_bytes.begin() + 2); + LogPrintLevel(BCLog::SV2, BCLog::Level::Info, "Template Provider authority key: %s\n", EncodeBase58Check(version_pubkey_bytes)); + LogTrace(BCLog::SV2, "Authority key: %s\n", HexStr(m_authority_pubkey)); + + // Generate and sign certificate + auto now{GetTime()}; + uint16_t version = 0; + // Start validity a little bit in the past to account for clock difference + uint32_t valid_from = static_cast(std::chrono::duration_cast(now).count()) - 3600; + uint32_t valid_to = std::numeric_limits::max(); // 2106 + Sv2SignatureNoiseMessage certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, XOnlyPubKey(static_key.GetPubKey()), authority_key); + + // TODO: persist certificate + + m_connman = std::make_unique(TP_SUBPROTOCOL, static_key, m_authority_pubkey, certificate); + + // Suppress unused variable warning, result is unused. + m_mining.getTip(); +} + +bool Sv2TemplateProvider::Start(const Sv2TemplateProviderOptions& options) +{ + m_options = options; + + if (!m_connman->Start(this, m_options.host, m_options.port)) { + return false; + } + + m_thread_sv2_handler = std::thread(&util::TraceThread, "sv2", [this] { ThreadSv2Handler(); }); + return true; +} + +Sv2TemplateProvider::~Sv2TemplateProvider() +{ + AssertLockNotHeld(m_tp_mutex); + + m_connman->Interrupt(); + m_connman->StopThreads(); + + Interrupt(); + StopThreads(); +} + +void Sv2TemplateProvider::Interrupt() +{ + m_flag_interrupt_sv2 = true; +} + +void Sv2TemplateProvider::StopThreads() +{ + if (m_thread_sv2_handler.joinable()) { + m_thread_sv2_handler.join(); + } +} + +void Sv2TemplateProvider::ThreadSv2Handler() +{ + while (!m_flag_interrupt_sv2) { + // TODO: handle messages + } +} diff --git a/src/sv2/template_provider.h b/src/sv2/template_provider.h new file mode 100644 index 0000000000000..64612d65a6a6e --- /dev/null +++ b/src/sv2/template_provider.h @@ -0,0 +1,106 @@ +#ifndef BITCOIN_SV2_TEMPLATE_PROVIDER_H +#define BITCOIN_SV2_TEMPLATE_PROVIDER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +struct Sv2TemplateProviderOptions +{ + /** + * Running inside a test + */ + bool is_test{false}; + + /** + * Host for the server to bind to. + */ + std::string host{"127.0.0.1"}; + + /** + * The listening port for the server. + */ + uint16_t port{8336}; +}; + +/** + * The main class that runs the template provider server. + */ +class Sv2TemplateProvider : public Sv2EventsInterface +{ + +private: + /** + * The Mining interface is used to build new valid blocks, get the best known + * block hash and to check whether the node is still in IBD. + */ + interfaces::Mining& m_mining; + + /* + * The template provider subprotocol used in setup connection messages. The stratum v2 + * template provider only recognizes its own subprotocol. + */ + static constexpr uint8_t TP_SUBPROTOCOL{0x02}; + + std::unique_ptr m_connman; + + /** + * Configuration + */ + Sv2TemplateProviderOptions m_options; + + /** + * The main thread for the template provider. + */ + std::thread m_thread_sv2_handler; + + /** + * Signal for handling interrupts and stopping the template provider event loop. + */ + std::atomic m_flag_interrupt_sv2{false}; + CThreadInterrupt m_interrupt_sv2; + +public: + explicit Sv2TemplateProvider(interfaces::Mining& mining); + + ~Sv2TemplateProvider() EXCLUSIVE_LOCKS_REQUIRED(!m_tp_mutex); + + Mutex m_tp_mutex; + + /** + * Starts the template provider server and thread. + * returns false if port is unable to bind. + */ + [[nodiscard]] bool Start(const Sv2TemplateProviderOptions& options = {}); + + /** + * The main thread for the template provider, contains an event loop handling + * all tasks for the template provider. + */ + void ThreadSv2Handler(); + + /** + * Triggered on interrupt signals to stop the main event loop in ThreadSv2Handler(). + */ + void Interrupt(); + + /** + * Tear down of the template provider thread and any other necessary tear down. + */ + void StopThreads(); + + /** + * Main handler for all received stratum v2 messages. + */ + void ProcessSv2Message(const node::Sv2NetMsg& sv2_header, Sv2Client& client); + + // Only used for tests + XOnlyPubKey m_authority_pubkey; +}; + +#endif // BITCOIN_SV2_TEMPLATE_PROVIDER_H diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 31b6cf57a65e4..3ac2553ef56db 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -186,6 +186,7 @@ if(WITH_SV2) sv2_noise_tests.cpp sv2_transport_tests.cpp sv2_messages_tests.cpp + sv2_template_provider_tests.cpp ) target_link_libraries(test_bitcoin bitcoin_sv2) endif() diff --git a/src/test/sv2_template_provider_tests.cpp b/src/test/sv2_template_provider_tests.cpp new file mode 100644 index 0000000000000..c2b64049a2272 --- /dev/null +++ b/src/test/sv2_template_provider_tests.cpp @@ -0,0 +1,154 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +BOOST_FIXTURE_TEST_SUITE(sv2_template_provider_tests, TestChain100Setup) + +/** + * A class for testing the Template Provider. Each TPTester encapsulates a + * Sv2TemplateProvider (the one being tested) as well as a Sv2Cipher + * to act as the other side. + */ +class TPTester { +private: + std::unique_ptr m_peer_transport; //!< Transport for peer + // Sockets that will be returned by the TP listening socket Accept() method. + std::shared_ptr m_tp_accepted_sockets{std::make_shared()}; + std::shared_ptr m_current_client_pipes; + +public: + std::unique_ptr m_tp; //!< Sv2TemplateProvider being tested + Sv2TemplateProviderOptions m_tp_options{.is_test = true}; //! Options passed to the TP + + TPTester(interfaces::Mining& mining) + { + m_tp = std::make_unique(mining); + + CreateSock = [this](int, int, int) -> std::unique_ptr { + // This will be the bind/listen socket from m_tp. It will + // create other sockets via its Accept() method. + return std::make_unique(std::make_shared(), m_tp_accepted_sockets); + }; + + BOOST_REQUIRE(m_tp->Start(m_tp_options)); + } + + void SendPeerBytes() + { + const auto& [data, more, _m_message_type] = m_peer_transport->GetBytesToSend(/*have_next_message=*/false); + BOOST_REQUIRE(data.size() > 0); + + // Schedule data to be returned by the next Recv() call from + // Sv2Connman on the socket it has accepted. + m_current_client_pipes->recv.PushBytes(data.data(), data.size()); + m_peer_transport->MarkBytesSent(data.size()); + } + + // Have the peer receive and process bytes: + size_t PeerReceiveBytes() + { + uint8_t buf[0x10000]; + // Get the data that has been written to the accepted socket with Send() by TP. + // Wait until the bytes appear in the "send" pipe. + ssize_t n; + for (;;) { + n = m_current_client_pipes->send.GetBytes(buf, sizeof(buf), 0); + if (n != -1 || errno != EAGAIN) { + break; + } + UninterruptibleSleep(50ms); + } + + // Inform client's transport that some bytes have been received (sent by TP). + if (n > 0) { + std::span s(buf, n); + BOOST_REQUIRE(m_peer_transport->ReceivedBytes(s)); + } + + return n; + } + + /* Create a new client and perform handshake */ + void handshake() + { + m_peer_transport.reset(); + + auto peer_static_key{GenerateRandomKey()}; + m_peer_transport = std::make_unique(std::move(peer_static_key), m_tp->m_authority_pubkey); + + // Have Sv2Connman's listen socket's Accept() simulate a newly arrived connection. + m_current_client_pipes = std::make_shared(); + m_tp_accepted_sockets->Push( + std::make_unique(m_current_client_pipes, std::make_shared())); + + // Flush transport for handshake part 1 + SendPeerBytes(); + + // Read handshake part 2 from transport + BOOST_REQUIRE_EQUAL(PeerReceiveBytes(), Sv2HandshakeState::HANDSHAKE_STEP2_SIZE); + } + + void receiveMessage(Sv2NetMsg& msg) + { + // Client encrypts message and puts it on the transport: + CSerializedNetMsg net_msg{std::move(msg)}; + BOOST_REQUIRE(m_peer_transport->SetMessageToSend(net_msg)); + SendPeerBytes(); + } + + Sv2NetMsg SetupConnectionMsg() + { + std::vector bytes{ + 0x02, // protocol + 0x02, 0x00, // min_version + 0x02, 0x00, // max_version + 0x01, 0x00, 0x00, 0x00, // flags + 0x07, 0x30, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x30, // endpoint_host + 0x61, 0x21, // endpoint_port + 0x07, 0x42, 0x69, 0x74, 0x6d, 0x61, 0x69, 0x6e, // vendor + 0x08, 0x53, 0x39, 0x69, 0x20, 0x31, 0x33, 0x2e, 0x35, // hardware_version + 0x1c, 0x62, 0x72, 0x61, 0x69, 0x69, 0x6e, 0x73, 0x2d, 0x6f, 0x73, 0x2d, 0x32, 0x30, + 0x31, 0x38, 0x2d, 0x30, 0x39, 0x2d, 0x32, 0x32, 0x2d, 0x31, 0x2d, 0x68, 0x61, 0x73, + 0x68, // firmware + 0x10, 0x73, 0x6f, 0x6d, 0x65, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2d, 0x75, + 0x75, 0x69, 0x64, // device_id + }; + + return node::Sv2NetMsg{node::Sv2MsgType::SETUP_CONNECTION, std::move(bytes)}; + } +}; + +BOOST_AUTO_TEST_CASE(client_tests) +{ + auto mining{interfaces::MakeMining(m_node)}; + TPTester tester{*mining}; + + 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); + + 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)}; + // No reply expected, not yet implemented + tester.receiveMessage(msg); +} + +BOOST_AUTO_TEST_SUITE_END() From 20f673876ea6cd71660428130ee196e33f672e02 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 29 Nov 2023 18:06:59 +0100 Subject: [PATCH 2/9] Chainparams: add default sv2 port Co-authored-by: Christopher Coverdale --- src/chainparamsbase.cpp | 10 +++++----- src/chainparamsbase.h | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/chainparamsbase.cpp b/src/chainparamsbase.cpp index d816d1af91c35..1084292905693 100644 --- a/src/chainparamsbase.cpp +++ b/src/chainparamsbase.cpp @@ -41,15 +41,15 @@ std::unique_ptr CreateBaseChainParams(const ChainType chain) { switch (chain) { case ChainType::MAIN: - return std::make_unique("", 8332); + return std::make_unique("", 8332, 8336); case ChainType::TESTNET: - return std::make_unique("testnet3", 18332); + return std::make_unique("testnet3", 18332, 18336); case ChainType::TESTNET4: - return std::make_unique("testnet4", 48332); + return std::make_unique("testnet4", 48332, 48336); case ChainType::SIGNET: - return std::make_unique("signet", 38332); + return std::make_unique("signet", 38332, 38336); case ChainType::REGTEST: - return std::make_unique("regtest", 18443); + return std::make_unique("regtest", 18443, 18447); } assert(false); } diff --git a/src/chainparamsbase.h b/src/chainparamsbase.h index adbd6a5174695..d7decc39ea18d 100644 --- a/src/chainparamsbase.h +++ b/src/chainparamsbase.h @@ -22,13 +22,15 @@ class CBaseChainParams public: const std::string& DataDir() const { return strDataDir; } uint16_t RPCPort() const { return m_rpc_port; } + uint16_t Sv2Port() const { return m_sv2_port; } CBaseChainParams() = delete; - CBaseChainParams(const std::string& data_dir, uint16_t rpc_port) - : m_rpc_port(rpc_port), strDataDir(data_dir) {} + CBaseChainParams(const std::string& data_dir, uint16_t rpc_port, uint16_t sv2_port) + : m_rpc_port(rpc_port), m_sv2_port(sv2_port), strDataDir(data_dir) {} private: const uint16_t m_rpc_port; + const uint16_t m_sv2_port; std::string strDataDir; }; From 0dc1a78b3ffa874cc1b3486a96985d7a851988f4 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 24 Jun 2024 14:22:05 +0200 Subject: [PATCH 3/9] Add remaining sv2 messages for TemplateProvider --- src/sv2/CMakeLists.txt | 1 + src/sv2/messages.cpp | 41 ++++ src/sv2/messages.h | 358 +++++++++++++++++++++++++++++++- src/test/sv2_messages_tests.cpp | 143 +++++++++++++ 4 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 src/sv2/messages.cpp diff --git a/src/sv2/CMakeLists.txt b/src/sv2/CMakeLists.txt index 7b632032c233f..0504ea2a5c51f 100644 --- a/src/sv2/CMakeLists.txt +++ b/src/sv2/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(bitcoin_sv2 STATIC EXCLUDE_FROM_ALL noise.cpp transport.cpp connman.cpp + messages.cpp template_provider.cpp ) diff --git a/src/sv2/messages.cpp b/src/sv2/messages.cpp new file mode 100644 index 0000000000000..c5881ea8983fc --- /dev/null +++ b/src/sv2/messages.cpp @@ -0,0 +1,41 @@ +#include + +#include +#include +#include + +node::Sv2NewTemplateMsg::Sv2NewTemplateMsg(const CBlockHeader& header, const CTransactionRef coinbase_tx, std::vector coinbase_merkle_path, int witness_commitment_index, uint64_t template_id, bool future_template) + : m_template_id{template_id}, m_future_template{future_template} +{ + m_version = header.nVersion; + + m_coinbase_tx_version = coinbase_tx->CURRENT_VERSION; + m_coinbase_prefix = coinbase_tx->vin[0].scriptSig; + m_coinbase_tx_input_sequence = coinbase_tx->vin[0].nSequence; + + // The coinbase nValue already contains the nFee + the Block Subsidy when built using CreateBlock(). + m_coinbase_tx_value_remaining = static_cast(coinbase_tx->vout[0].nValue); + + m_coinbase_tx_outputs_count = 0; + if (witness_commitment_index != NO_WITNESS_COMMITMENT) { + m_coinbase_tx_outputs_count = 1; + + std::vector coinbase_tx_outputs{coinbase_tx->vout[witness_commitment_index]}; + m_coinbase_tx_outputs = coinbase_tx_outputs; + } + + m_coinbase_tx_locktime = coinbase_tx->nLockTime; + + for (const auto& hash : coinbase_merkle_path) { + m_merkle_path.push_back(hash); + } + +} + +node::Sv2SetNewPrevHashMsg::Sv2SetNewPrevHashMsg(const CBlockHeader& header, uint64_t template_id) : m_template_id{template_id} +{ + m_prev_hash = header.hashPrevBlock; + m_header_timestamp = header.nTime; + m_nBits = header.nBits; + m_target = ArithToUint256(arith_uint256().SetCompact(header.nBits)); +} diff --git a/src/sv2/messages.h b/src/sv2/messages.h index c04c95c1dd5c7..1a35dcc9ed0bd 100644 --- a/src/sv2/messages.h +++ b/src/sv2/messages.h @@ -35,13 +35,25 @@ enum class Sv2MsgType : uint8_t { SETUP_CONNECTION = 0x00, SETUP_CONNECTION_SUCCESS = 0x01, SETUP_CONNECTION_ERROR = 0x02, - COINBASE_OUTPUT_CONSTRAINTS = 0x70 + NEW_TEMPLATE = 0x71, + SET_NEW_PREV_HASH = 0x72, + REQUEST_TRANSACTION_DATA = 0x73, + REQUEST_TRANSACTION_DATA_SUCCESS = 0x74, + REQUEST_TRANSACTION_DATA_ERROR = 0x75, + SUBMIT_SOLUTION = 0x76, + COINBASE_OUTPUT_CONSTRAINTS = 0x70, }; static const std::map SV2_MSG_NAMES{ {Sv2MsgType::SETUP_CONNECTION, "SetupConnection"}, {Sv2MsgType::SETUP_CONNECTION_SUCCESS, "SetupConnectionSuccess"}, {Sv2MsgType::SETUP_CONNECTION_ERROR, "SetupConnectionError"}, + {Sv2MsgType::NEW_TEMPLATE, "NewTemplate"}, + {Sv2MsgType::SET_NEW_PREV_HASH, "SetNewPrevHash"}, + {Sv2MsgType::REQUEST_TRANSACTION_DATA, "RequestTransactionData"}, + {Sv2MsgType::REQUEST_TRANSACTION_DATA_SUCCESS, "RequestTransactionData.Success"}, + {Sv2MsgType::REQUEST_TRANSACTION_DATA_ERROR, "RequestTransactionData.Error"}, + {Sv2MsgType::SUBMIT_SOLUTION, "SubmitSolution"}, {Sv2MsgType::COINBASE_OUTPUT_CONSTRAINTS, "CoinbaseOutputConstraints"}, }; @@ -227,6 +239,350 @@ struct Sv2SetupConnectionErrorMsg } }; +/** + * The work template for downstream devices. Can be used for future work or immediate work. + * The NewTemplate will be matched to a cached block using the template id. + */ +struct Sv2NewTemplateMsg +{ + /** + * The default message type value for this Stratum V2 message. + */ + static constexpr auto m_msg_type = Sv2MsgType::NEW_TEMPLATE; + + /** + * Server’s identification of the template. Strictly increasing, the current UNIX + * time may be used in place of an ID. + */ + uint64_t m_template_id; + + /** + * True if the template is intended for future SetNewPrevHash message sent on the channel. + * If False, the job relates to the last sent SetNewPrevHash message on the channel + * and the miner should start to work on the job immediately. + */ + bool m_future_template; + + /** + * Valid header version field that reflects the current network consensus. + * The general purpose bits (as specified in BIP320) can be freely manipulated + * by the downstream node. The downstream node MUST NOT rely on the upstream + * node to set the BIP320 bits to any particular value. + */ + uint32_t m_version; + + /** + * The coinbase transaction nVersion field. + */ + uint32_t m_coinbase_tx_version; + + /** + * Up to 8 bytes (not including the length byte) which are to be placed at + * the beginning of the coinbase field in the coinbase transaction. + */ + CScript m_coinbase_prefix; + + /** + * The coinbase transaction input’s nSequence field. + */ + uint32_t m_coinbase_tx_input_sequence; + + /** + * The value, in satoshis, available for spending in coinbase outputs added + * by the client. Includes both transaction fees and block subsidy. + */ + uint64_t m_coinbase_tx_value_remaining; + + /** + * The number of transaction outputs included in coinbase_tx_outputs. + */ + uint32_t m_coinbase_tx_outputs_count; + + /** + * Bitcoin transaction outputs to be included as the last outputs in the coinbase transaction. + */ + std::vector m_coinbase_tx_outputs; + + /** + * The locktime field in the coinbase transaction. + */ + uint32_t m_coinbase_tx_locktime; + + /** + * Merkle path hashes ordered from deepest. + */ + std::vector m_merkle_path; + + Sv2NewTemplateMsg() = default; + explicit Sv2NewTemplateMsg(const CBlockHeader& header, const CTransactionRef coinbase_tx, std::vector coinbase_merkle_path, int witness_commitment_index, uint64_t template_id, bool future_template); + + template + void Serialize(Stream& s) const + { + s << m_template_id + << m_future_template + << m_version + << m_coinbase_tx_version + << m_coinbase_prefix + << m_coinbase_tx_input_sequence + << m_coinbase_tx_value_remaining + << m_coinbase_tx_outputs_count; + + // If there are more than 0 coinbase tx outputs, then we need to serialize them + // as [B0_64K](https://github.com/stratum-mining/sv2-spec/blob/main/03-Protocol-Overview.md#31-data-types-mapping) + if (m_coinbase_tx_outputs_count > 0) { + std::vector outputs_bytes; + // TODO: support more than 1 output + VectorWriter{outputs_bytes, 0, m_coinbase_tx_outputs.at(0)}; + + s << static_cast(outputs_bytes.size()); + s.write(MakeByteSpan(outputs_bytes)); + } else { + // We will still need to send 2 bytes indicating an empty coinbase-tx_outputs array as a B0_64K. + s << static_cast(0); + } + + s << m_coinbase_tx_locktime + // Technically using VARINT is a problem for a merkle path longer + // than 127 elements, but a block can't have enough transactions to + // run into that. + << m_merkle_path; + } +}; + +/** + * When the template provider creates a new valid best block, the template provider + * MUST immediately send the SetNewPrevHash message. This message can also be used + * for a future template, indicating the client can begin work on a previously + * received and cached NewTemplate which contains the same template id. + */ +struct Sv2SetNewPrevHashMsg +{ + /** + * The default message type value for this Stratum V2 message. + */ + static constexpr auto m_msg_type = Sv2MsgType::SET_NEW_PREV_HASH; + + /** + * The id referenced in a previous NewTemplate message. + */ + uint64_t m_template_id; + + /** + * Previous block’s hash, as it must appear in the next block’s header. + */ + uint256 m_prev_hash; + + /** + * The nTime field in the block header at which the client should start (usually current time). + * This is NOT the minimum valid nTime value. + */ + uint32_t m_header_timestamp; + + /** + * Block header field. + */ + uint32_t m_nBits; + + /** + * The maximum double-SHA256 hash value which would represent a valid block. + * Note that this may be lower than the target implied by nBits in several cases, + * including weak-block based block propagation. + */ + uint256 m_target; + + Sv2SetNewPrevHashMsg() = default; + explicit Sv2SetNewPrevHashMsg(const CBlockHeader& header, uint64_t template_id); + + template + void Serialize(Stream& s) const + { + s << m_template_id + << m_prev_hash + << m_header_timestamp + << m_nBits + << m_target; + } +}; + +/** + * The client (usually a Job Negotiator) sends a RequestTransactionData message + * to the Template Provider asking for the full set of transaction data (excluding + * the coinbase) in the block and any additional data relevant for validation + * associated with the template_id. + */ +struct Sv2RequestTransactionDataMsg +{ + /** + * The default message type value for this Stratum V2 message. + */ + static constexpr auto m_msg_type = Sv2MsgType::REQUEST_TRANSACTION_DATA; + + /** + * The template_id corresponding to a NewTemplate message. + */ + uint64_t m_template_id; + + Sv2RequestTransactionDataMsg() = default; + + template + void Unserialize(Stream& s) + { + s >> m_template_id; + } +}; + +/** + * A message for a successful request for transaction data. It contains the full + * serialized transaction data from a NewTemplate according to the id. + */ +struct Sv2RequestTransactionDataSuccessMsg +{ + /** + * The default message type value for this Stratum V2 message. + */ + static constexpr auto m_msg_type = Sv2MsgType::REQUEST_TRANSACTION_DATA_SUCCESS; + + /** + * The template_id corresponding to a NewTemplate message. + */ + uint64_t m_template_id; + + /** + * Extra data which the Pool may require to validate the work + */ + std::vector m_excess_data; + + /** + * List of full transactions requested by client found in the + * corresponding template. + */ + std::vector m_transactions_list; + + Sv2RequestTransactionDataSuccessMsg() = default; + + explicit Sv2RequestTransactionDataSuccessMsg(uint64_t template_id, std::vector&& excess_data, std::vector&& transactions_list) : m_template_id{template_id}, m_excess_data{excess_data}, m_transactions_list{transactions_list} {}; + + template + void Serialize(Stream& s) const + { + s << m_template_id; + + // excess data is expected to be serialized as a B0_64K type. + if (m_excess_data.empty()) { + s << static_cast(0); + } else { + s << static_cast(m_excess_data.size()); + s.write(MakeByteSpan(m_excess_data)); + } + + // transactions list is expected to be serialized as a SEQ0_64K[B0_16M]. + s << static_cast(m_transactions_list.size()); + for (const auto& tx : m_transactions_list) { + DataStream ss_tx{}; + ss_tx << TX_WITH_WITNESS(*tx); + auto tx_size = static_cast(ss_tx.size()); + + u24_t tx_byte_len; + tx_byte_len[2] = (tx_size >> 16) & 0xff; + tx_byte_len[1] = (tx_size >> 8) & 0xff; + tx_byte_len[0] = tx_size & 0xff; + + s << tx_byte_len; + s.write(MakeByteSpan(ss_tx)); + } + }; +}; + +/** + * The error message for the client if the template provider is unable to send + * the full serialized transaction data. + */ +struct Sv2RequestTransactionDataErrorMsg +{ + /** + * The default message type value for this Stratum V2 message. + */ + static constexpr auto m_msg_type = Sv2MsgType::REQUEST_TRANSACTION_DATA_ERROR; + + /** + * The template_id corresponding to a NewTemplate/RequestTransactionData message. + */ + uint64_t m_template_id; + + /** + * Human-readable error code on why no transaction data has been provided. + */ + std::string m_error_code; + + explicit Sv2RequestTransactionDataErrorMsg(uint64_t template_id, std::string&& error_code) : m_template_id{template_id}, m_error_code{error_code} {}; + + template + void Serialize(Stream& s) const + { + s << m_template_id + << m_error_code; + } +}; + +/** + * The client sends a SubmitSolution after finding a coinbase transaction/nonce + * pair which double-SHA256 hashes at or below SetNewPrevHash::target. The template provider + * finds the cached block according to the template id and reconstructs the block with the + * values from SubmitSolution. The template provider must then propagate the block to the + * Bitcoin Network. + */ +struct Sv2SubmitSolutionMsg +{ + /** + * The default message type value for this Stratum V2 message. + */ + static constexpr auto m_msg_type = Sv2MsgType::SUBMIT_SOLUTION; + + /** + * The id referenced in a NewTemplate. + */ + uint64_t m_template_id; + + /** + * The version field in the block header. Bits not defined by BIP320 as additional + * nonce MUST be the same as they appear in the NewWork message, other bits may + * be set to any value. + */ + uint32_t m_version; + + /** + * The nTime field in the block header. This MUST be greater than or equal to + * the header_timestamp field in the latest SetNewPrevHash message and lower + * than or equal to that value plus the number of seconds since the receipt + * of that message. + */ + uint32_t m_header_timestamp; + + /** + * The nonce field in the header. + */ + uint32_t m_header_nonce; + + /** + * The full serialized coinbase transaction, meeting all the requirements of the NewWork message, above. + */ + CMutableTransaction m_coinbase_tx; + + Sv2SubmitSolutionMsg() = default; + + template + void Unserialize(Stream& s) + { + s >> m_template_id >> m_version >> m_header_timestamp >> m_header_nonce; + + // Ignore the 2 byte length as the rest of the stream is assumed to be + // the m_coinbase_tx. + s.ignore(2); + s >> TX_WITH_WITNESS(m_coinbase_tx); + } +}; + /** * Header for all stratum v2 messages. Each header must contain the message type, * the length of the serialized message and a 2 byte extension field currently diff --git a/src/test/sv2_messages_tests.cpp b/src/test/sv2_messages_tests.cpp index 68d1a7009b500..f51fb6bac8169 100644 --- a/src/test/sv2_messages_tests.cpp +++ b/src/test/sv2_messages_tests.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -97,4 +98,146 @@ BOOST_AUTO_TEST_CASE(Sv2SetupConnectionError_test) BOOST_CHECK_EQUAL(HexStr(ss), expected); } + +BOOST_AUTO_TEST_CASE(Sv2NewTemplate_test) +{ + // NewTemplate + // https://github.com/stratum-mining/sv2-spec/blob/main/07-Template-Distribution-Protocol.md#72-newtemplate-server---client + // + // U64 0100000000000000 template_id + // BOOL 00 future_template + // U32 00000030 version + // U32 02000000 coinbase tx version + // B0_255 04 coinbase_prefix len + // 03012100 coinbase prefix + // U32 ffffffff coinbase tx input sequence + // U64 0040075af0750700 coinbase tx value remaining + // U32 01000000 coinbase tx outputs count + // B0_64K 0c coinbase_tx_outputs (concatenated, total bytes) + // 000100000000000000 amount + // 036a012a len(script) + script + // U32 dbc80d00 coinbase lock time (height 903,387) + // SEQ0_255[U256] 01 merkle path length + // 1a6240823de4c8d6aaf826851bdf2b0e8d5acf7c31e8578cff4c394b5a32bd4e - merkle path + std::string expected{"01000000000000000000000030020000000403012100ffffffff0040075af0750700010000000c000100000000000000036a012adbc80d00011a6240823de4c8d6aaf826851bdf2b0e8d5acf7c31e8578cff4c394b5a32bd4e"}; + + node::Sv2NewTemplateMsg new_template; + new_template.m_template_id = 1; + new_template.m_future_template = false; + new_template.m_version = 805306368; + new_template.m_coinbase_tx_version = 2; + + std::vector coinbase_prefix{0x03, 0x01, 0x21, 0x00}; + CScript prefix(coinbase_prefix.begin(), coinbase_prefix.end()); + new_template.m_coinbase_prefix = prefix; + + new_template.m_coinbase_tx_input_sequence = 4294967295; + new_template.m_coinbase_tx_value_remaining = MAX_MONEY; + + // Create fake witness commitment + new_template.m_coinbase_tx_outputs_count = 1; + std::vector coinbase_tx_ouputs{CTxOut(1, CScript() << OP_RETURN << 42)}; + new_template.m_coinbase_tx_outputs = coinbase_tx_ouputs; + + new_template.m_coinbase_tx_locktime = 903387; + + std::vector merkle_path; + CMutableTransaction mtx_tx; + CTransaction tx{mtx_tx}; + merkle_path.push_back(tx.GetHash().ToUint256()); + new_template.m_merkle_path = merkle_path; + + DataStream ss{}; + ss << new_template; + + BOOST_CHECK_EQUAL(HexStr(ss), expected); +} + +BOOST_AUTO_TEST_CASE(Sv2NetHeader_NewTemplate_test) +{ + // 0000 - extension type + // 71 - msg type (NewTemplate) + // 000000 - msg length + std::string expected{"000071000000"}; + node::Sv2NetHeader sv2_header{node::Sv2MsgType::NEW_TEMPLATE, 0}; + + DataStream ss{}; + ss << sv2_header; + + BOOST_CHECK_EQUAL(HexStr(ss), expected); +} + + +BOOST_AUTO_TEST_CASE(Sv2SetNewPrevHash_test) +{ + // 0200000000000000 - template_id + // e59a2ef8d4826b89fe637f3b 5322c0f167fd2d3dc88ec568a7c2c8ffd8eb8873 - prev hash + // 973c0e63 - header timestamp + // ffff7f03 - nbits + // ffff7f0000000000000000000000000000000000000000000000000000000000 - target + std::string expected{"0200000000000000e59a2ef8d4826b89fe637f3b5322c0f167fd2d3dc88ec568a7c2c8ffd8eb8873973c0e63ffff7f03ffff7f0000000000000000000000000000000000000000000000000000000000"}; + + std::vector prev_hash_input{ + 0xe5, 0x9a, 0x2e, 0xf8, 0xd4, 0x82, 0x6b, 0x89, 0xfe, 0x63, 0x7f, 0x3b, + 0x53, 0x22, 0xc0, 0xf1, 0x67, 0xfd, 0x2d, 0x3d, 0xc8, 0x8e, 0xc5, 0x68, + 0xa7, 0xc2, 0xc8, 0xff, 0xd8, 0xeb, 0x88, 0x73}; + uint256 prev_hash = uint256(prev_hash_input); + + CBlockHeader header; + header.hashPrevBlock = prev_hash; + header.nTime = 1661877399; + header.nBits = 58720255; + + node::Sv2SetNewPrevHashMsg new_prev_hash{header, 2}; + + DataStream ss{}; + ss << new_prev_hash; + + BOOST_CHECK_EQUAL(HexStr(ss), expected); +} + +BOOST_AUTO_TEST_CASE(Sv2NetHeader_SetNewPrevHash_test) +{ + // 0000 - extension type + // 72 - msg type (SetNewPrevHash) + // 000000 - msg length + std::string expected{"000072000000"}; + node::Sv2NetHeader sv2_header{node::Sv2MsgType::SET_NEW_PREV_HASH, 0}; + + DataStream ss{}; + ss << sv2_header; + + BOOST_CHECK_EQUAL(HexStr(ss), expected); +} + +BOOST_AUTO_TEST_CASE(Sv2SubmitSolution_test) +{ + uint8_t input[]{ + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // template_id + 0x02, 0x00, 0x00, 0x00, // version + 0x97, 0x3c, 0x0e, 0x63, // header_timestamp + 0xff, 0xff, 0x7f, 0x03, // header_nonce + 0x5d, 0x00, // 2 byte length of coinbase_tx + 0x2, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, // coinbase_tx + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, + 0xff, 0x22, 0x1, 0x18, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x16, + 0x0, 0x14, 0x53, 0x12, 0x60, 0xaa, 0x2a, 0x19, 0x9e, + 0x22, 0x8c, 0x53, 0x7d, 0xfa, 0x42, 0xc8, 0x2b, 0xea, + 0x2c, 0x7c, 0x1f, 0x4d, 0x0, 0x0, 0x0, 0x0}; + DataStream ss(input); + + node::Sv2SubmitSolutionMsg submit_solution; + ss >> submit_solution; + + // BOOST_CHECK_EQUAL(submit_solution.m_template_id, 2); + // BOOST_CHECK_EQUAL(submit_solution.m_version, 2); + // BOOST_CHECK_EQUAL(submit_solution.m_header_timestamp, 1661877399); + // BOOST_CHECK_EQUAL(submit_solution.m_header_nonce, 58720255); +} BOOST_AUTO_TEST_SUITE_END() From cb6a3e5aae2dce4beb630df7fb078cf243a22bbf Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 22 Nov 2024 14:41:56 +0100 Subject: [PATCH 4/9] Sv2: construct and submit block templates Incrementally update the template. --- src/sv2/connman.h | 6 + src/sv2/template_provider.cpp | 277 ++++++++++++++++++++++- src/sv2/template_provider.h | 73 +++++- src/test/sv2_template_provider_tests.cpp | 132 ++++++++++- 4 files changed, 481 insertions(+), 7 deletions(-) diff --git a/src/sv2/connman.h b/src/sv2/connman.h index eee7c8ab98d2c..3f232c9bd723f 100644 --- a/src/sv2/connman.h +++ b/src/sv2/connman.h @@ -59,6 +59,12 @@ struct Sv2Client */ unsigned int m_coinbase_tx_outputs_size; + /** + * Tracks the best template in the Template Provider m_block_template_cache map. + * Not guaranteed to still exist. + */ + uint64_t m_best_template_id = 0; + explicit Sv2Client(size_t id, std::unique_ptr transport) : m_id{id}, m_transport{std::move(transport)} {}; diff --git a/src/sv2/template_provider.cpp b/src/sv2/template_provider.cpp index 89a6f38ca1ab9..cc769c1560032 100644 --- a/src/sv2/template_provider.cpp +++ b/src/sv2/template_provider.cpp @@ -36,9 +36,6 @@ Sv2TemplateProvider::Sv2TemplateProvider(interfaces::Mining& mining) : m_mining{ // TODO: persist certificate m_connman = std::make_unique(TP_SUBPROTOCOL, static_key, m_authority_pubkey, certificate); - - // Suppress unused variable warning, result is unused. - m_mining.getTip(); } bool Sv2TemplateProvider::Start(const Sv2TemplateProviderOptions& options) @@ -76,9 +73,281 @@ void Sv2TemplateProvider::StopThreads() } } +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()}; + if (now - m_last_triggered >= m_interval) { + m_last_triggered = now; + return true; + } + return false; + } + + void reset() { + auto now{GetTime()}; + m_last_triggered = now; + } +}; + void Sv2TemplateProvider::ThreadSv2Handler() { + // Make sure it's initialized, doesn't need to be accurate. + { + LOCK(m_tp_mutex); + m_last_block_time = GetTime(); + } + + // Wait to come out of IBD, except on signet, where we might be the only miner. + while (!m_flag_interrupt_sv2 && gArgs.GetChainType() != ChainType::SIGNET) { + // TODO: Wait until there's no headers-only branch with more work than our chaintip. + // The current check can still cause us to broadcast a few dozen useless templates + // at startup. + if (!m_mining.isInitialBlockDownload()) break; + LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Waiting to come out of IBD\n"); + std::this_thread::sleep_for(1000ms); + } + + std::map client_threads; + while (!m_flag_interrupt_sv2) { - // TODO: handle messages + // We start with one template per client, which has an interface through + // which we monitor for better templates. + + m_connman->ForEachClient([this, &client_threads](Sv2Client& client) { + /** + * The initial handshake is handled on the Sv2Connman thread. This + * consists of the noise protocol handshake and the initial Stratum + * v2 messages SetupConnection and CoinbaseOutputConstraints. + * + * A further refactor should make that part non-blocking. But for + * now we spin up a thread here. + */ + if (!client.m_coinbase_output_constraints_recv) return; + + if (client_threads.contains(client.m_id)) return; + + client_threads.emplace(client.m_id, + std::thread(&util::TraceThread, + strprintf("sv2-%zu", client.m_id), + [this, &client] { ThreadSv2ClientHandler(client.m_id); })); + }); + + // Take a break (handling new connections is not urgent) + std::this_thread::sleep_for(100ms); + + LOCK(m_tp_mutex); + PruneBlockTemplateCache(); } + + for (auto& thread : client_threads) { + if (thread.second.joinable()) { + // If the node is shutting down, then all pending waitNext() calls + // should return in under a second. + thread.second.join(); + } + } + + +} + +void Sv2TemplateProvider::ThreadSv2ClientHandler(size_t client_id) +{ + Timer timer(m_options.fee_check_interval); + std::shared_ptr block_template; + + while (!m_flag_interrupt_sv2) { + if (!block_template) { + LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Generate initial block template for client id=%zu\n", + client_id); + + // Create block template and store interface reference + // TODO: reuse template_id for clients with the same coinbase constraints + uint64_t template_id{WITH_LOCK(m_tp_mutex, return ++m_template_id;)}; + + node::BlockCreateOptions options {.use_mempool = true}; + { + LOCK(m_connman->m_clients_mutex); + std::shared_ptr client = m_connman->GetClientById(client_id); + if (!client) break; + + // The node enforces a minimum of 2000, though not for IPC so we could go a bit + // lower, but let's not... + options.block_reserved_weight = 2000 + client->m_coinbase_tx_outputs_size * 4; + } + + const auto time_start{SteadyClock::now()}; + block_template = m_mining.createNewBlock(options); + if (!block_template) { + LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "No new template for client id=%zu, node is shutting down\n", + client_id); + break; + } + + LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Assemble template: %.2fms\n", + Ticks(SteadyClock::now() - time_start)); + + uint256 prev_hash{block_template->getBlockHeader().hashPrevBlock}; + { + LOCK(m_tp_mutex); + if (prev_hash != m_best_prev_hash) { + m_best_prev_hash = prev_hash; + // Does not need to be accurate + m_last_block_time = GetTime(); + } + + // Add template to cache before sending it, to prevent race + // condition: https://github.com/stratum-mining/stratum/issues/1773 + m_block_template_cache.insert({template_id,block_template}); + } + + { + LOCK(m_connman->m_clients_mutex); + std::shared_ptr client = m_connman->GetClientById(client_id); + if (!client) break; + + if (!SendWork(*client, template_id, *block_template, /*future_template=*/true)) { + LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Disconnecting client id=%zu\n", + client_id); + LOCK(client->cs_status); + client->m_disconnect_flag = true; + } + } + + timer.reset(); + } + + // The future template flag is set when there's a new prevhash, + // not when there's only a fee increase. + bool future_template{false}; + + // -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}; + + node::BlockWaitOptions options; + options.fee_threshold = fee_delta; + if (!check_fees) { + options.timeout = m_options.fee_check_interval; + LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Ignore fee changes for -sv2interval seconds, wait for a new tip, client id=%zu\n", + client_id); + } else { + if (m_options.is_test) { + options.timeout = MillisecondsDouble(1000); + } + LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Wait for fees to rise by %d sat or a new tip, client id=%zu\n", + fee_delta, client_id); + } + + uint256 old_prev_hash{block_template->getBlockHeader().hashPrevBlock}; + std::shared_ptr tmpl = block_template->waitNext(options); + // The client may have disconnected during the wait, check now to avoid + // a spurious IPC call and confusing log statements. + { + LOCK(m_connman->m_clients_mutex); + if (!m_connman->GetClientById(client_id)) break; + } + + if (tmpl) { + block_template = tmpl; + uint256 new_prev_hash{block_template->getBlockHeader().hashPrevBlock}; + + { + LOCK(m_tp_mutex); + if (new_prev_hash != old_prev_hash) { + LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Tip changed, client id=%zu\n", + client_id); + future_template = true; + m_best_prev_hash = new_prev_hash; + // Does not need to be accurate + m_last_block_time = GetTime(); + } + + ++m_template_id; + + // Add template to cache before sending it, to prevent race + // condition: https://github.com/stratum-mining/stratum/issues/1773 + m_block_template_cache.insert({m_template_id,block_template}); + } + + { + LOCK(m_connman->m_clients_mutex); + std::shared_ptr client = m_connman->GetClientById(client_id); + if (!client) break; + + if (!SendWork(*client, WITH_LOCK(m_tp_mutex, return m_template_id;), *block_template, future_template)) { + LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Disconnecting client id=%zu\n", + client_id); + LOCK(client->cs_status); + client->m_disconnect_flag = true; + } + } + + timer.reset(); + } else { + // In production this only happens during shutdown, in tests timeouts are expected. + LogPrintLevel(BCLog::SV2, BCLog::Level::Trace, "Timeout for client id=%zu\n", + client_id); + } + + if (m_options.is_test) { + // Take a break + std::this_thread::sleep_for(50ms); + } + } +} + +void Sv2TemplateProvider::PruneBlockTemplateCache() +{ + AssertLockHeld(m_tp_mutex); + + // Allow a few seconds for clients to submit a block + auto recent = GetTime() - std::chrono::seconds(10); + if (m_last_block_time > recent) return; + // If the blocks prevout is not the tip's prevout, delete it. + uint256 prev_hash = m_best_prev_hash; + std::erase_if(m_block_template_cache, [prev_hash] (const auto& kv) { + if (kv.second->getBlockHeader().hashPrevBlock != prev_hash) { + return true; + } + return false; + }); +} + +bool Sv2TemplateProvider::SendWork(Sv2Client& client, uint64_t template_id, BlockTemplate& block_template, bool future_template) +{ + CBlockHeader header{block_template.getBlockHeader()}; + + node::Sv2NewTemplateMsg new_template{header, + block_template.getCoinbaseTx(), + block_template.getCoinbaseMerklePath(), + block_template.getWitnessCommitmentIndex(), + template_id, + future_template}; + + // TODO: use optimistic send instead of adding to the queue + + LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Send 0x71 NewTemplate id=%lu future=%d to client id=%zu\n", template_id, future_template, client.m_id); + LOCK(client.cs_send); + client.m_send_messages.emplace_back(new_template); + + if (future_template) { + node::Sv2SetNewPrevHashMsg new_prev_hash{header, template_id}; + LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Send 0x72 SetNewPrevHash to client id=%zu\n", client.m_id); + client.m_send_messages.emplace_back(new_prev_hash); + } + + return true; } diff --git a/src/sv2/template_provider.h b/src/sv2/template_provider.h index 64612d65a6a6e..8ffcc47532a5f 100644 --- a/src/sv2/template_provider.h +++ b/src/sv2/template_provider.h @@ -1,6 +1,7 @@ #ifndef BITCOIN_SV2_TEMPLATE_PROVIDER_H #define BITCOIN_SV2_TEMPLATE_PROVIDER_H +#include #include #include #include @@ -10,6 +11,8 @@ #include #include +using interfaces::BlockTemplate; + struct Sv2TemplateProviderOptions { /** @@ -26,6 +29,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}; }; /** @@ -65,6 +78,28 @@ class Sv2TemplateProvider : public Sv2EventsInterface std::atomic m_flag_interrupt_sv2{false}; CThreadInterrupt m_interrupt_sv2; + /** + * The most recent template id. This is incremented on creating new template, + * which happens for each connected client. + */ + uint64_t m_template_id GUARDED_BY(m_tp_mutex){0}; + + /** + * The current best known block hash in the network. + */ + uint256 m_best_prev_hash GUARDED_BY(m_tp_mutex){uint256(0)}; + + /** When we last saw a new block connection. Used to cache stale templates + * for some time after this. + */ + std::chrono::nanoseconds m_last_block_time GUARDED_BY(m_tp_mutex); + + /** + * A cache that maps ids used in NewTemplate messages and its associated block template. + */ + using BlockTemplateCache = std::map>; + BlockTemplateCache m_block_template_cache GUARDED_BY(m_tp_mutex); + public: explicit Sv2TemplateProvider(interfaces::Mining& mining); @@ -82,7 +117,19 @@ class Sv2TemplateProvider : public Sv2EventsInterface * The main thread for the template provider, contains an event loop handling * all tasks for the template provider. */ - void ThreadSv2Handler(); + void ThreadSv2Handler() EXCLUSIVE_LOCKS_REQUIRED(!m_tp_mutex); + + /** + * Give each client its own thread so they're treated equally + * and so that newly connected clients don't have to wait. + * This scales very poorly, because block template creation is + * slow, but is easier to reason about. + * + * A typical miner as well as a typical pool will only need one + * connection. For the use case of a public facing template provider, + * further changes are needed anyway e.g. for DoS resistance. + */ + void ThreadSv2ClientHandler(size_t client_id) EXCLUSIVE_LOCKS_REQUIRED(!m_tp_mutex); /** * Triggered on interrupt signals to stop the main event loop in ThreadSv2Handler(). @@ -97,10 +144,32 @@ class Sv2TemplateProvider : public Sv2EventsInterface /** * Main handler for all received stratum v2 messages. */ - void ProcessSv2Message(const node::Sv2NetMsg& sv2_header, Sv2Client& client); + void ProcessSv2Message(const node::Sv2NetMsg& sv2_header, Sv2Client& client) EXCLUSIVE_LOCKS_REQUIRED(!m_tp_mutex); // Only used for tests XOnlyPubKey m_authority_pubkey; + + /* Block templates that connected clients may be working on, only used for tests */ + BlockTemplateCache& GetBlockTemplates() EXCLUSIVE_LOCKS_REQUIRED(m_tp_mutex) { return m_block_template_cache; } + +private: + + /* Forget templates from before the last block, but with a few seconds margin. */ + void PruneBlockTemplateCache() EXCLUSIVE_LOCKS_REQUIRED(m_tp_mutex); + + /** + * Sends the best NewTemplate and SetNewPrevHash to a client. + * + * The current implementation doesn't create templates for future empty + * or speculative blocks. Despite that, we first send NewTemplate with + * future_template set to true, followed by SetNewPrevHash. We do this + * both when first connecting and when a new block is found. + * + * When the template is update to take newer mempool transactions into + * account, we set future_template to false and don't send SetNewPrevHash. + */ + [[nodiscard]] bool SendWork(Sv2Client& client, uint64_t template_id, BlockTemplate& block_template, bool future_template); + }; #endif // BITCOIN_SV2_TEMPLATE_PROVIDER_H diff --git a/src/test/sv2_template_provider_tests.cpp b/src/test/sv2_template_provider_tests.cpp index c2b64049a2272..0f3af2f630fd7 100644 --- a/src/test/sv2_template_provider_tests.cpp +++ b/src/test/sv2_template_provider_tests.cpp @@ -1,13 +1,21 @@ +#include #include #include +#include +#include #include #include #include #include +#include #include +#include #include +// For verbose debugging use: +// build/src/test/test_bitcoin --run_test=sv2_template_provider_tests --log_level=all -- -debug=sv2 -loglevel=sv2:trace -printtoconsole=1 | grep -v disabled + BOOST_FIXTURE_TEST_SUITE(sv2_template_provider_tests, TestChain100Setup) /** @@ -122,6 +130,12 @@ class TPTester { return node::Sv2NetMsg{node::Sv2MsgType::SETUP_CONNECTION, std::move(bytes)}; } + + size_t GetBlockTemplateCount() + { + LOCK(m_tp->m_tp_mutex); + return m_tp->GetBlockTemplates().size(); + } }; BOOST_AUTO_TEST_CASE(client_tests) @@ -142,13 +156,129 @@ BOOST_AUTO_TEST_CASE(client_tests) // SetupConnection.Success is 6 bytes BOOST_REQUIRE_EQUAL(tester.PeerReceiveBytes(), SV2_HEADER_ENCRYPTED_SIZE + 6 + Poly1305::TAGLEN); + // 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)}; - // No reply expected, not yet implemented tester.receiveMessage(msg); + BOOST_TEST_MESSAGE("The reply should be NewTemplate and SetNewPrevHash"); + BOOST_REQUIRE_EQUAL(tester.PeerReceiveBytes(), 2 * SV2_HEADER_ENCRYPTED_SIZE + 91 + 80 + 2 * Poly1305::TAGLEN); + + // There should now be one template + BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 1); + + // Move mock time + // If the mempool doesn't change, no new template is generated. + SetMockTime(GetMockTime() + std::chrono::seconds{10}); + BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 1); + + // Create a transaction with a large fee + 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); + + 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 block creation + UninterruptibleSleep(std::chrono::milliseconds{200}); + + // 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_TEST_MESSAGE("Receive NewTemplate"); + BOOST_REQUIRE_EQUAL(tester.PeerReceiveBytes(), expected_len); + + // Get the latest template id + uint64_t template_id = 0; + { + LOCK(tester.m_tp->m_tp_mutex); + for (auto& t : tester.m_tp->GetBlockTemplates()) { + if (t.first > template_id) { + template_id = t.first; + } + } + } + + BOOST_REQUIRE_EQUAL(template_id, 2); + + { + 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); + } + + UninterruptibleSleep(std::chrono::milliseconds{200}); + + // 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}); + + // 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); + + // Check that there's a new template + BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 3); + + BOOST_TEST_MESSAGE("Create a new block"); + mineBlocks(1); + + UninterruptibleSleep(std::chrono::milliseconds{200}); + + // 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 + 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 + // before a block is found, so ASIC's can preload it. We would + // then immedidately send a SetNewPrevHash message when there's + // a new block, and construct a better template _after_ that. + + // Templates are briefly preserved + BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 4); + + // Do not provide transactions for stale templates + // TODO + + // But do allow SubmitSolution + // TODO + + // Until after some time + SetMockTime(GetMockTime() + std::chrono::seconds{15}); + UninterruptibleSleep(std::chrono::milliseconds{1100}); + BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 1); + + // Mine a block in order to interrupt waitNext() + mineBlocks(1); } BOOST_AUTO_TEST_SUITE_END() From 0acfb7672f10fcff48d68df0bc4d9b4783189538 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 2 Jul 2024 15:13:49 +0200 Subject: [PATCH 5/9] Handle REQUEST_TRANSACTION_DATA --- src/sv2/connman.cpp | 16 ++++++++ src/sv2/connman.h | 7 ++++ src/sv2/template_provider.cpp | 52 ++++++++++++++++++++++++ src/sv2/template_provider.h | 2 + src/test/sv2_connman_tests.cpp | 4 ++ src/test/sv2_template_provider_tests.cpp | 33 ++++++++++++++- 6 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/sv2/connman.cpp b/src/sv2/connman.cpp index dc4c10dcd9ef8..dca073adf5eb3 100644 --- a/src/sv2/connman.cpp +++ b/src/sv2/connman.cpp @@ -361,6 +361,22 @@ void Sv2Connman::ProcessSv2Message(const Sv2NetMsg& sv2_net_msg, Sv2Client& clie break; } + case Sv2MsgType::REQUEST_TRANSACTION_DATA: + { + node::Sv2RequestTransactionDataMsg request_tx_data; + + try { + ss >> request_tx_data; + } catch (const std::exception& e) { + LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Received invalid RequestTransactionData message from client id=%zu: %e\n", + client.m_id, e.what()); + return; + } + + m_msgproc->RequestTransactionData(client, request_tx_data); + + break; + } default: { uint8_t msg_type[1]{uint8_t(sv2_net_msg.m_msg_type)}; LogPrintLevel(BCLog::SV2, BCLog::Level::Warning, "Received unknown message type 0x%s from client id=%zu\n", diff --git a/src/sv2/connman.h b/src/sv2/connman.h index 3f232c9bd723f..ace7c5c8e0c0f 100644 --- a/src/sv2/connman.h +++ b/src/sv2/connman.h @@ -83,6 +83,13 @@ struct Sv2Client class Sv2EventsInterface { public: + /** + * We received and successfully parsed a RequestTransactionData message. + * Deal with it and respond with either RequestTransactionData.Success or + * RequestTransactionData.Error. + */ + virtual void RequestTransactionData(Sv2Client& client, node::Sv2RequestTransactionDataMsg msg) = 0; + virtual ~Sv2EventsInterface() = default; }; diff --git a/src/sv2/template_provider.cpp b/src/sv2/template_provider.cpp index cc769c1560032..02fd5e14533c7 100644 --- a/src/sv2/template_provider.cpp +++ b/src/sv2/template_provider.cpp @@ -309,6 +309,58 @@ void Sv2TemplateProvider::ThreadSv2ClientHandler(size_t client_id) } } +void Sv2TemplateProvider::RequestTransactionData(Sv2Client& client, node::Sv2RequestTransactionDataMsg msg) +{ + CBlock block; + { + LOCK(m_tp_mutex); + auto cached_block = m_block_template_cache.find(msg.m_template_id); + if (cached_block == m_block_template_cache.end()) { + node::Sv2RequestTransactionDataErrorMsg request_tx_data_error{msg.m_template_id, "template-id-not-found"}; + + LogDebug(BCLog::SV2, "Send 0x75 RequestTransactionData.Error (template-id-not-found: %zu) to client id=%zu\n", + msg.m_template_id, client.m_id); + LOCK(client.cs_send); + client.m_send_messages.emplace_back(request_tx_data_error); + + return; + } + block = (*cached_block->second).getBlock(); + } + + { + LOCK(m_tp_mutex); + if (block.hashPrevBlock != m_best_prev_hash) { + LogTrace(BCLog::SV2, "Template id=%lu prevhash=%s, tip=%s\n", msg.m_template_id, HexStr(block.hashPrevBlock), HexStr(m_best_prev_hash)); + node::Sv2RequestTransactionDataErrorMsg request_tx_data_error{msg.m_template_id, "stale-template-id"}; + + + LogDebug(BCLog::SV2, "Send 0x75 RequestTransactionData.Error (stale-template-id) to client id=%zu\n", + client.m_id); + LOCK(client.cs_send); + client.m_send_messages.emplace_back(request_tx_data_error); + return; + } + } + + std::vector witness_reserve_value; + auto scriptWitness = block.vtx[0]->vin[0].scriptWitness; + if (!scriptWitness.IsNull()) { + std::copy(scriptWitness.stack[0].begin(), scriptWitness.stack[0].end(), std::back_inserter(witness_reserve_value)); + } + std::vector txs; + if (block.vtx.size() > 0) { + std::copy(block.vtx.begin() + 1, block.vtx.end(), std::back_inserter(txs)); + } + + node::Sv2RequestTransactionDataSuccessMsg request_tx_data_success{msg.m_template_id, std::move(witness_reserve_value), std::move(txs)}; + + LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Send 0x74 RequestTransactionData.Success to client id=%zu\n", + client.m_id); + LOCK(client.cs_send); + client.m_send_messages.emplace_back(request_tx_data_success); +} + void Sv2TemplateProvider::PruneBlockTemplateCache() { AssertLockHeld(m_tp_mutex); diff --git a/src/sv2/template_provider.h b/src/sv2/template_provider.h index 8ffcc47532a5f..293e1dfaeb15a 100644 --- a/src/sv2/template_provider.h +++ b/src/sv2/template_provider.h @@ -149,6 +149,8 @@ class Sv2TemplateProvider : public Sv2EventsInterface // Only used for tests XOnlyPubKey m_authority_pubkey; + void RequestTransactionData(Sv2Client& client, node::Sv2RequestTransactionDataMsg msg) EXCLUSIVE_LOCKS_REQUIRED(!m_tp_mutex) override; + /* Block templates that connected clients may be working on, only used for tests */ BlockTemplateCache& GetBlockTemplates() EXCLUSIVE_LOCKS_REQUIRED(m_tp_mutex) { return m_block_template_cache; } diff --git a/src/test/sv2_connman_tests.cpp b/src/test/sv2_connman_tests.cpp index af0c2ec34ad83..7ac7378d94bd2 100644 --- a/src/test/sv2_connman_tests.cpp +++ b/src/test/sv2_connman_tests.cpp @@ -155,6 +155,10 @@ class ConnTester : Sv2EventsInterface { return node::Sv2NetMsg{node::Sv2MsgType::SETUP_CONNECTION, std::move(bytes)}; } + void RequestTransactionData(Sv2Client& client, node::Sv2RequestTransactionDataMsg msg) override { + BOOST_TEST_MESSAGE("Process RequestTransactionData"); + } + }; BOOST_AUTO_TEST_CASE(client_tests) diff --git a/src/test/sv2_template_provider_tests.cpp b/src/test/sv2_template_provider_tests.cpp index 0f3af2f630fd7..cc48c886c7f15 100644 --- a/src/test/sv2_template_provider_tests.cpp +++ b/src/test/sv2_template_provider_tests.cpp @@ -178,6 +178,7 @@ BOOST_AUTO_TEST_CASE(client_tests) 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 @@ -191,6 +192,11 @@ BOOST_AUTO_TEST_CASE(client_tests) /*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); } @@ -219,6 +225,24 @@ BOOST_AUTO_TEST_CASE(client_tests) BOOST_REQUIRE_EQUAL(template_id, 2); + UninterruptibleSleep(std::chrono::milliseconds{200}); + + // Have the peer send us RequestTransactionData + // We should reply with RequestTransactionData.Success + node::Sv2NetHeader req_tx_data_header{node::Sv2MsgType::REQUEST_TRANSACTION_DATA, 8}; + DataStream ss; + ss << template_id; + std::vector template_id_bytes; + template_id_bytes.resize(8); + ss >> MakeWritableByteSpan(template_id_bytes); + + msg = node::Sv2NetMsg{req_tx_data_header.m_msg_type, std::move(template_id_bytes)}; + tester.receiveMessage(msg); + const size_t template_id_size = 8; + const size_t excess_data_size = 2 + 32; + size_t tx_list_size = 2 + 3 + tx_size; + BOOST_TEST_MESSAGE("Receive RequestTransactionData.Success"); + BOOST_REQUIRE_EQUAL(tester.PeerReceiveBytes(), SV2_HEADER_ENCRYPTED_SIZE + template_id_size + excess_data_size + tx_list_size + Poly1305::TAGLEN); { LOCK(cs_main); @@ -231,8 +255,6 @@ BOOST_AUTO_TEST_CASE(client_tests) BOOST_REQUIRE_EQUAL(m_node.mempool->size(), 1); } - UninterruptibleSleep(std::chrono::milliseconds{200}); - // Move mock time SetMockTime(GetMockTime() + std::chrono::seconds{tester.m_tp_options.fee_check_interval}); @@ -248,6 +270,13 @@ BOOST_AUTO_TEST_CASE(client_tests) // Check that there's a new template BOOST_REQUIRE_EQUAL(tester.GetBlockTemplateCount(), 3); + // 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); + BOOST_TEST_MESSAGE("Create a new block"); mineBlocks(1); From 7cda119bbcd55267fdcf5341550644f7b29e41ed Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 2 Jul 2024 15:14:02 +0200 Subject: [PATCH 6/9] Handle SUBMIT_SOLUTION --- src/sv2/connman.cpp | 22 +++++++++++++++++ src/sv2/connman.h | 5 ++++ src/sv2/template_provider.cpp | 45 ++++++++++++++++++++++++++++++++++ src/sv2/template_provider.h | 2 ++ src/test/sv2_connman_tests.cpp | 4 +++ 5 files changed, 78 insertions(+) diff --git a/src/sv2/connman.cpp b/src/sv2/connman.cpp index dca073adf5eb3..f9f9421462135 100644 --- a/src/sv2/connman.cpp +++ b/src/sv2/connman.cpp @@ -361,6 +361,28 @@ void Sv2Connman::ProcessSv2Message(const Sv2NetMsg& sv2_net_msg, Sv2Client& clie break; } + case Sv2MsgType::SUBMIT_SOLUTION: { + { + LOCK(client.cs_status); + if (!client.m_setup_connection_confirmed && !client.m_coinbase_output_constraints_recv) { + client.m_disconnect_flag = true; + return; + } + } + + node::Sv2SubmitSolutionMsg submit_solution; + try { + ss >> submit_solution; + } catch (const std::exception& e) { + LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Received invalid SubmitSolution message from client id=%zu: %e\n", + client.m_id, e.what()); + return; + } + + m_msgproc->SubmitSolution(submit_solution); + + break; + } case Sv2MsgType::REQUEST_TRANSACTION_DATA: { node::Sv2RequestTransactionDataMsg request_tx_data; diff --git a/src/sv2/connman.h b/src/sv2/connman.h index ace7c5c8e0c0f..4e6e1df17ed73 100644 --- a/src/sv2/connman.h +++ b/src/sv2/connman.h @@ -90,6 +90,11 @@ class Sv2EventsInterface */ virtual void RequestTransactionData(Sv2Client& client, node::Sv2RequestTransactionDataMsg msg) = 0; + /** + * We received and successfully parsed a SubmitSolution message. + */ + virtual void SubmitSolution(node::Sv2SubmitSolutionMsg solution) = 0; + virtual ~Sv2EventsInterface() = default; }; diff --git a/src/sv2/template_provider.cpp b/src/sv2/template_provider.cpp index 02fd5e14533c7..7032e8de1d223 100644 --- a/src/sv2/template_provider.cpp +++ b/src/sv2/template_provider.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -361,6 +362,50 @@ void Sv2TemplateProvider::RequestTransactionData(Sv2Client& client, node::Sv2Req client.m_send_messages.emplace_back(request_tx_data_success); } +void Sv2TemplateProvider::SubmitSolution(node::Sv2SubmitSolutionMsg solution) +{ + LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "id=%lu version=%d, timestamp=%d, nonce=%d\n", + solution.m_template_id, + solution.m_version, + solution.m_header_timestamp, + solution.m_header_nonce + ); + + std::shared_ptr block_template; + { + // We can't hold this lock until submitSolution() because it's + // possible that the new block arrives via the p2p network at the + // same time. That leads to a deadlock in g_best_block_mutex. + LOCK(m_tp_mutex); + auto cached_block_template = m_block_template_cache.find(solution.m_template_id); + if (cached_block_template == m_block_template_cache.end()) { + LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Template with id=%lu is no longer in cache\n", + solution.m_template_id); + return; + } + /** + * It's important to not delete this template from the cache in case + * another solution is submitted for the same template later. + * + * This is very unlikely on mainnet, but not impossible. Many mining + * devices may be working on the default pool template at the same + * time and they may not update the new tip right away. + * + * The node will never broadcast the second block. It's marked + * valid-headers in getchaintips. However a node or pool operator + * may wish to manually inspect the block or keep it as a souvenir. + * Additionally, because in Stratum v2 the block solution is sent + * to both the pool node and the template provider node, it's + * possibly they arrive out of order and two competing blocks propagate + * on the network. In case of a reorg the node will be able to switch + * faster because it already has (but not fully validated) the block. + */ + block_template = cached_block_template->second; + } + + block_template->submitSolution(solution.m_version, solution.m_header_timestamp, solution.m_header_nonce, MakeTransactionRef(solution.m_coinbase_tx)); +} + void Sv2TemplateProvider::PruneBlockTemplateCache() { AssertLockHeld(m_tp_mutex); diff --git a/src/sv2/template_provider.h b/src/sv2/template_provider.h index 293e1dfaeb15a..0b7e57e22d52e 100644 --- a/src/sv2/template_provider.h +++ b/src/sv2/template_provider.h @@ -151,6 +151,8 @@ class Sv2TemplateProvider : public Sv2EventsInterface void RequestTransactionData(Sv2Client& client, node::Sv2RequestTransactionDataMsg msg) EXCLUSIVE_LOCKS_REQUIRED(!m_tp_mutex) override; + void SubmitSolution(node::Sv2SubmitSolutionMsg solution) EXCLUSIVE_LOCKS_REQUIRED(!m_tp_mutex) override; + /* Block templates that connected clients may be working on, only used for tests */ BlockTemplateCache& GetBlockTemplates() EXCLUSIVE_LOCKS_REQUIRED(m_tp_mutex) { return m_block_template_cache; } diff --git a/src/test/sv2_connman_tests.cpp b/src/test/sv2_connman_tests.cpp index 7ac7378d94bd2..1f173f1911c7c 100644 --- a/src/test/sv2_connman_tests.cpp +++ b/src/test/sv2_connman_tests.cpp @@ -159,6 +159,10 @@ class ConnTester : Sv2EventsInterface { BOOST_TEST_MESSAGE("Process RequestTransactionData"); } + void SubmitSolution(node::Sv2SubmitSolutionMsg solution) override { + BOOST_TEST_MESSAGE("Process SubmitSolution"); + } + }; BOOST_AUTO_TEST_CASE(client_tests) From 995618ef498410f5146ca2f07de68cc29fdac98c Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Thu, 1 Feb 2024 14:18:49 +0100 Subject: [PATCH 7/9] CKey: add Serialize and Unserialize Co-authored-by: Vasil Dimov --- src/key.h | 26 ++++++++++++++++++++++++++ src/test/key_tests.cpp | 23 +++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/key.h b/src/key.h index 22f96880b7b07..2b36df160ac3f 100644 --- a/src/key.h +++ b/src/key.h @@ -204,6 +204,7 @@ class CKey ECDHSecret ComputeBIP324ECDHSecret(const EllSwiftPubKey& their_ellswift, const EllSwiftPubKey& our_ellswift, bool initiating) const; + /** Compute a KeyPair * * Wraps a `secp256k1_keypair` type. @@ -220,6 +221,31 @@ class CKey * Merkle root of the script tree). */ KeyPair ComputeKeyPair(const uint256* merkle_root) const; + + /** Straight-forward serialization of key bytes (and compressed flag). + * Use GetPrivKey() for OpenSSL compatible DER encoding. + */ + template + void Serialize(Stream& s) const + { + if (!IsValid()) { + throw std::ios_base::failure("invalid key"); + } + s << fCompressed; + ::Serialize(s, std::span{*this}); + } + + template + void Unserialize(Stream& s) + { + s >> fCompressed; + MakeKeyData(); + s >> std::span{*keydata}; + if (!Check(keydata->data())) { + ClearKeyData(); + throw std::ios_base::failure("invalid key"); + } + } }; CKey GenerateRandomKey(bool compressed = true) noexcept; diff --git a/src/test/key_tests.cpp b/src/test/key_tests.cpp index 1b60a9c9eba6d..d64a7c05c9965 100644 --- a/src/test/key_tests.cpp +++ b/src/test/key_tests.cpp @@ -390,4 +390,27 @@ BOOST_AUTO_TEST_CASE(key_schnorr_tweak_smoke_test) secp256k1_context_destroy(secp256k1_context_sign); } +BOOST_AUTO_TEST_CASE(key_serialization) +{ + { + DataStream s{}; + CKey key; + BOOST_CHECK_EXCEPTION(s << key, std::ios_base::failure, + HasReason{"invalid key"}); + + s << MakeByteSpan(std::vector(33, std::byte(0))); + BOOST_CHECK_EXCEPTION(s >> key, std::ios_base::failure, + HasReason{"invalid key"}); + } + + for (bool compressed : {true, false}) { + CKey key{GenerateRandomKey(/*compressed=*/compressed)}; + DataStream s{}; + s << key; + CKey key_copy; + s >> key_copy; + BOOST_CHECK(key == key_copy); + } +} + BOOST_AUTO_TEST_SUITE_END() From f209f5cc680f2a727c17727e148a853a16fef770 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Thu, 11 Jan 2024 15:58:48 +0100 Subject: [PATCH 8/9] Persist static key for Template Provider --- src/sv2/template_provider.cpp | 63 +++++++++++++++++++++++++++++++---- src/sv2/template_provider.h | 6 ++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/sv2/template_provider.cpp b/src/sv2/template_provider.cpp index 7032e8de1d223..911a59cc74632 100644 --- a/src/sv2/template_provider.cpp +++ b/src/sv2/template_provider.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -13,10 +14,52 @@ Sv2TemplateProvider::Sv2TemplateProvider(interfaces::Mining& mining) : m_mining{ { // TODO: persist static key CKey static_key; - static_key.MakeNewKey(true); - - auto authority_key{GenerateRandomKey()}; - + try { + AutoFile{fsbridge::fopen(GetStaticKeyFile(), "rb")} >> static_key; + LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Reading cached static key from %s\n", fs::PathToString(GetStaticKeyFile())); + } catch (const std::ios_base::failure&) { + // File is not expected to exist the first time. + // In the unlikely event that loading an existing key fails, create a new one. + } + if (!static_key.IsValid()) { + static_key = GenerateRandomKey(); + try { + AutoFile static_key_file{fsbridge::fopen(GetStaticKeyFile(), "wb")}; + static_key_file << static_key; + // Ignore failure to close + (void)static_key_file.fclose(); + } catch (const std::ios_base::failure&) { + LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Error writing static key to %s\n", fs::PathToString(GetStaticKeyFile())); + // Continue, because this is not a critical failure. + } + LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Generated static key, saved to %s\n", fs::PathToString(GetStaticKeyFile())); + } + LogPrintLevel(BCLog::SV2, BCLog::Level::Info, "Static key: %s\n", HexStr(static_key.GetPubKey())); + + // Generate self signed certificate using (cached) authority key + // TODO: skip loading authoritity key if -sv2cert is used + + // Load authority key if cached + CKey authority_key; + try { + AutoFile{fsbridge::fopen(GetAuthorityKeyFile(), "rb")} >> authority_key; + } catch (const std::ios_base::failure&) { + // File is not expected to exist the first time. + // In the unlikely event that loading an existing key fails, create a new one. + } + if (!authority_key.IsValid()) { + authority_key = GenerateRandomKey(); + try { + AutoFile authority_key_file{fsbridge::fopen(GetAuthorityKeyFile(), "wb")}; + authority_key_file << authority_key; + // Ignore failure to close + (void)authority_key_file.fclose(); + } catch (const std::ios_base::failure&) { + LogPrintLevel(BCLog::SV2, BCLog::Level::Error, "Error writing authority key to %s\n", fs::PathToString(GetAuthorityKeyFile())); + // Continue, because this is not a critical failure. + } + LogPrintLevel(BCLog::SV2, BCLog::Level::Debug, "Generated authority key, saved to %s\n", fs::PathToString(GetAuthorityKeyFile())); + } // SRI uses base58 encoded x-only pubkeys in its configuration files std::array version_pubkey_bytes; version_pubkey_bytes[0] = 1; @@ -34,11 +77,19 @@ Sv2TemplateProvider::Sv2TemplateProvider(interfaces::Mining& mining) : m_mining{ uint32_t valid_to = std::numeric_limits::max(); // 2106 Sv2SignatureNoiseMessage certificate = Sv2SignatureNoiseMessage(version, valid_from, valid_to, XOnlyPubKey(static_key.GetPubKey()), authority_key); - // TODO: persist certificate - m_connman = std::make_unique(TP_SUBPROTOCOL, static_key, m_authority_pubkey, certificate); } +fs::path Sv2TemplateProvider::GetStaticKeyFile() +{ + return gArgs.GetDataDirNet() / "sv2_static_key"; +} + +fs::path Sv2TemplateProvider::GetAuthorityKeyFile() +{ + return gArgs.GetDataDirNet() / "sv2_authority_key"; +} + bool Sv2TemplateProvider::Start(const Sv2TemplateProviderOptions& options) { m_options = options; diff --git a/src/sv2/template_provider.h b/src/sv2/template_provider.h index 0b7e57e22d52e..7f62191852998 100644 --- a/src/sv2/template_provider.h +++ b/src/sv2/template_provider.h @@ -62,6 +62,12 @@ class Sv2TemplateProvider : public Sv2EventsInterface std::unique_ptr m_connman; + /** Get name of file to store static key */ + fs::path GetStaticKeyFile(); + + /** Get name of file to store authority key */ + fs::path GetAuthorityKeyFile(); + /** * Configuration */ From ce5f206905286256c45de422f9d6aab8ec5be4b2 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 8 Apr 2025 14:23:10 -0400 Subject: [PATCH 9/9] mining: add -coinbaselocktime On by default. Allow Stratum v2 miners to opt out, pending more discussion on the BIP. --- src/init.cpp | 1 + src/node/miner.cpp | 9 +++++++-- src/node/miner.h | 2 ++ src/policy/policy.h | 2 ++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index b6b52e2cea5c8..5429e25afabc6 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -676,6 +676,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) argsman.AddArg("-blockreservedweight=", strprintf("Reserve space for the fixed-size block header plus the largest coinbase transaction the mining software may add to the block. (default: %d).", DEFAULT_BLOCK_RESERVED_WEIGHT), ArgsManager::ALLOW_ANY, OptionsCategory::BLOCK_CREATION); argsman.AddArg("-blockmintxfee=", strprintf("Set lowest fee rate (in %s/kvB) for transactions to be included in block creation. (default: %s)", CURRENCY_UNIT, FormatMoney(DEFAULT_BLOCK_MIN_TX_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::BLOCK_CREATION); argsman.AddArg("-blockversion=", "Override block version to test forking scenarios", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::BLOCK_CREATION); + argsman.AddArg("-coinbaselocktime", strprintf("Set nLockTime to the current block height and nSequence to enforce it (default: %d)", DEFAULT_COINBASE_LOCKTIME), ArgsManager::ALLOW_ANY, OptionsCategory::BLOCK_CREATION); argsman.AddArg("-rest", strprintf("Accept public REST requests (default: %u)", DEFAULT_REST_ENABLE), ArgsManager::ALLOW_ANY, OptionsCategory::RPC); argsman.AddArg("-rpcallowip=", "Allow JSON-RPC connections from specified source. Valid values for are a single IP (e.g. 1.2.3.4), a network/netmask (e.g. 1.2.3.4/255.255.255.0), a network/CIDR (e.g. 1.2.3.4/24), all ipv4 (0.0.0.0/0), or all ipv6 (::/0). RFC4193 is allowed only if -cjdnsreachable=0. This option can be specified multiple times", ArgsManager::ALLOW_ANY, OptionsCategory::RPC); diff --git a/src/node/miner.cpp b/src/node/miner.cpp index a08c70e29daec..3cd8c4721e396 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -102,6 +102,7 @@ void ApplyArgsManOptions(const ArgsManager& args, BlockAssembler::Options& optio } options.print_modified_fee = args.GetBoolArg("-printpriority", options.print_modified_fee); options.block_reserved_weight = args.GetIntArg("-blockreservedweight", options.block_reserved_weight); + options.coinbase_locktime = args.GetBoolArg("-coinbaselocktime", DEFAULT_COINBASE_LOCKTIME); } void BlockAssembler::resetBlock() @@ -160,13 +161,17 @@ std::unique_ptr BlockAssembler::CreateNewBlock() CMutableTransaction coinbaseTx; coinbaseTx.vin.resize(1); coinbaseTx.vin[0].prevout.SetNull(); - coinbaseTx.vin[0].nSequence = CTxIn::MAX_SEQUENCE_NONFINAL; // Make sure timelock is enforced. + if (m_options.coinbase_locktime) { + coinbaseTx.vin[0].nSequence = CTxIn::MAX_SEQUENCE_NONFINAL; // Make sure timelock is enforced. + } coinbaseTx.vout.resize(1); coinbaseTx.vout[0].scriptPubKey = m_options.coinbase_output_script; coinbaseTx.vout[0].nValue = nFees + GetBlockSubsidy(nHeight, chainparams.GetConsensus()); coinbaseTx.vin[0].scriptSig = CScript() << nHeight << OP_0; Assert(nHeight > 0); - coinbaseTx.nLockTime = static_cast(nHeight - 1); + if (m_options.coinbase_locktime) { + coinbaseTx.nLockTime = static_cast(nHeight - 1); + } pblock->vtx[0] = MakeTransactionRef(std::move(coinbaseTx)); pblocktemplate->vchCoinbaseCommitment = m_chainstate.m_chainman.GenerateCoinbaseCommitment(*pblock, pindexPrev); diff --git a/src/node/miner.h b/src/node/miner.h index a9a88b39cf2c2..18fcb49daa0df 100644 --- a/src/node/miner.h +++ b/src/node/miner.h @@ -173,6 +173,8 @@ class BlockAssembler // Configuration parameters for the block size size_t nBlockMaxWeight{DEFAULT_BLOCK_MAX_WEIGHT}; CFeeRate blockMinFeeRate{DEFAULT_BLOCK_MIN_TX_FEE}; + // Whether to set nLockTime to the current height + bool coinbase_locktime{DEFAULT_COINBASE_LOCKTIME}; // Whether to call TestBlockValidity() at the end of CreateNewBlock(). bool test_block_validity{true}; bool print_modified_fee{DEFAULT_PRINT_MODIFIED_FEE}; diff --git a/src/policy/policy.h b/src/policy/policy.h index 23993dd705e5f..8942733f775b2 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -30,6 +30,8 @@ static constexpr unsigned int DEFAULT_BLOCK_RESERVED_WEIGHT{8000}; static constexpr unsigned int MINIMUM_BLOCK_RESERVED_WEIGHT{2000}; /** Default for -blockmintxfee, which sets the minimum feerate for a transaction in blocks created by mining code **/ static constexpr unsigned int DEFAULT_BLOCK_MIN_TX_FEE{1}; +/** Default for -coinbaselocktime, which sets the coinbase nLockTime (and nSequence) in blocks created by mining code **/ +static constexpr bool DEFAULT_COINBASE_LOCKTIME{true}; /** The maximum weight for transactions we're willing to relay/mine */ static constexpr int32_t MAX_STANDARD_TX_WEIGHT{400000}; /** The minimum non-witness size for transactions we're willing to relay/mine: one larger than 64 */