From 0859a72f867857d581d11c43ce64917db765c514 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Tue, 16 Jun 2026 13:39:13 +0000 Subject: [PATCH 1/4] dgb: minimal compiling skeleton + c2pool-dgb CMake target (slice #4) Author a genuinely minimal src/impl/dgb/node.cpp + src/c2pool/main_dgb.cpp and add the c2pool-dgb add_executable target (mirrors c2pool-btc) so the DGB binary builds and links off master. COMPILING SKELETON ONLY -- the embedded daemon + pool/sharechain body (PPLNS) is M3 / Phase B per the embedded-daemon roadmap (DGB = PORT-not-activation; Phase A p2p+mempool already landed under impl/dgb/coin/). The target compiles node.cpp directly and links only json + Boost: the skeleton references no core/coin/pool/ltc symbols, so it needs no coin lib and the top-level OBJECT-lib registration of src/impl/dgb (ci-steward convention PR) is not raced here. When the pool-pillars node.cpp lands (Phase B) it replaces the skeleton via a clean dgb-only re-cut off master. Scope: src/impl/dgb/ + the CMake target add only. No edits to src/impl/dash or src/impl/btc. Builds, links, and runs (--version / --help) verified locally. --- src/c2pool/CMakeLists.txt | 20 ++++++++++++++ src/c2pool/main_dgb.cpp | 56 +++++++++++++++++++++++++++++++++++++++ src/impl/dgb/node.cpp | 41 ++++++++++++++++++++++++++++ src/impl/dgb/node.hpp | 15 +++++++++++ 4 files changed, 132 insertions(+) create mode 100644 src/c2pool/main_dgb.cpp create mode 100644 src/impl/dgb/node.cpp diff --git a/src/c2pool/CMakeLists.txt b/src/c2pool/CMakeLists.txt index 540b94124..dc6738568 100644 --- a/src/c2pool/CMakeLists.txt +++ b/src/c2pool/CMakeLists.txt @@ -155,6 +155,26 @@ target_link_libraries(c2pool-btc ${Boost_LIBRARIES} ) +# c2pool-dgb: DigiByte Scrypt-only (V36) — slice #4 minimal COMPILING SKELETON. +# Mirrors the c2pool-btc add_executable shape, pruned to a stub entry. The body +# (embedded daemon + pool pillars) is M3/Phase B; this target only proves the +# binary builds + links off master. It compiles impl/dgb/node.cpp directly +# rather than linking a dgb / dgb_coin / impl_dgb lib on purpose: the top-level +# OBJECT-lib registration of src/impl/dgb is ci-steward's convention PR and is +# intentionally NOT raced here. The skeleton references NO core/coin/pool/ltc +# symbols (only std + the header-only dgb config), so it needs no coin lib and +# sidesteps the core/web_server self-link tangle that forces c2pool-btc to drag +# in ltc/payout/merged. Link deps stay at json + Boost until Phase B. +add_executable(c2pool-dgb + main_dgb.cpp + ${CMAKE_SOURCE_DIR}/src/impl/dgb/node.cpp +) +target_compile_definitions(c2pool-dgb PRIVATE C2POOL_VERSION="${C2POOL_GIT_VERSION}") +target_link_libraries(c2pool-dgb + nlohmann_json::nlohmann_json + ${Boost_LIBRARIES} +) + # Windows: Boost.Asio requires Winsock libraries; /bigobj for large translation units if(WIN32) target_link_libraries(c2pool ws2_32 mswsock bcrypt dbghelp) diff --git a/src/c2pool/main_dgb.cpp b/src/c2pool/main_dgb.cpp new file mode 100644 index 000000000..042b3eb82 --- /dev/null +++ b/src/c2pool/main_dgb.cpp @@ -0,0 +1,56 @@ +// c2pool-dgb — DigiByte Scrypt-only (V36) p2pool node entry point. +// +// SLICE #4 (Option B): genuinely minimal COMPILING SKELETON. This wires the +// CMake c2pool-dgb target so the binary builds + links off master; it does +// NOT yet run a node. The embedded-daemon + pool/sharechain body is M3 / +// Phase B (DGB = PORT-not-activation; Phase A p2p+mempool already landed under +// impl/dgb/coin/). When the pool-pillars node.cpp lands it replaces the +// skeleton via a clean dgb-only re-cut off master. +// +// V36 scope: Scrypt blocks validated; the other 4 DGB algos (SHA256d, Skein, +// Qubit, Odocrypt) are accept-by-continuity / ignored — full 5-algo support +// is V37. Compatibility target: frstrtr/p2pool-merged-v36 (share format, +// sharechain rules, PPLNS, Stratum, block submission). See +// c2pool-dgb-embedded-impl-plan.md (frstrtr/the docs/v36). +// +// Mirrors src/c2pool/main_btc.cpp's target shape, pruned to a stub entry. + +#include + +#include +#include + +#ifndef C2POOL_VERSION +#define C2POOL_VERSION "dev" +#endif + +namespace { + +void print_banner(const char* argv0) +{ + std::cout + << "c2pool-dgb " << C2POOL_VERSION << " — DigiByte Scrypt-only (V36)\n\n" + << "Usage: " << argv0 << " [--version] [--help]\n\n" + << "Status: skeleton (slice #4). The node run-loop (embedded daemon +\n" + << " pool pillars) lands in M3 / Phase B.\n" + << "Network: " << dgb::network_summary() << "\n"; +} + +} // namespace + +int main(int argc, char** argv) +{ + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--version") == 0) { + std::cout << "c2pool-dgb " << C2POOL_VERSION << "\n"; + return 0; + } + if (std::strcmp(argv[i], "--help") == 0) { + print_banner(argv[0]); + return 0; + } + } + + print_banner(argv[0]); + return dgb::run_skeleton(); +} diff --git a/src/impl/dgb/node.cpp b/src/impl/dgb/node.cpp new file mode 100644 index 000000000..8ea40e7b5 --- /dev/null +++ b/src/impl/dgb/node.cpp @@ -0,0 +1,41 @@ +#include "node.hpp" + +#include + +// c2pool-dgb node skeleton (slice #4, Option B). +// +// COMPILING SKELETON ONLY. The real embedded-daemon + pool/sharechain body +// (DensePPLNSWindow / PPLNS, p2p run-loop, template builder, broadcaster) is +// M3 / Phase B per the embedded-daemon roadmap: DGB is PORT-not-activation — +// Phase A p2p + mempool already landed under impl/dgb/coin/, and Phase B +// brings the pool pillars. When the pool-pillars node.cpp lands it REPLACES +// this file via a clean dgb-only re-cut off master; see +// c2pool-dgb-embedded-impl-plan.md (frstrtr/the docs/v36). +// +// Deliberately free of pool logic so c2pool-dgb LINKS today without dragging +// Phase B forward, instantiating the Fileconfig-derived config classes (whose +// load()/get_default() bodies are Phase B), or touching the shared +// bitcoin_family / DOGE-aux surface. + +namespace dgb +{ + +// One-line summary of the DGB-Scrypt network this binary targets. Reads the +// compile-time constants from config_{coin,pool}.hpp (header-only) — no +// Fileconfig instantiation. +std::string network_summary() +{ + return std::string("DigiByte Scrypt-only (V36) — ") + + "pool_p2p_port=" + std::to_string(PoolConfig::P2P_PORT) + + " coin_p2p_port=" + std::to_string(CoinParams::MAINNET_P2P_PORT) + + " block_period=" + std::to_string(CoinParams::BLOCK_PERIOD) + "s" + + " gbt_algo=" + std::string(CoinParams::GBT_ALGO); +} + +// Skeleton entry point. Phase B replaces this with the real node run-loop. +int run_skeleton() +{ + return 0; +} + +} // namespace dgb diff --git a/src/impl/dgb/node.hpp b/src/impl/dgb/node.hpp index d4980df4b..d8f777502 100644 --- a/src/impl/dgb/node.hpp +++ b/src/impl/dgb/node.hpp @@ -15,6 +15,8 @@ #include "config_pool.hpp" #include "config_coin.hpp" +#include + // DGB Scrypt shares use the same format as LTC shares. // The share type, validation logic, tracker, and protocol are shared. // Only config_pool and config_coin differ between LTC and DGB networks. @@ -27,4 +29,17 @@ namespace dgb using PoolConfigType = dgb::PoolConfig; using CoinParamsType = dgb::CoinParams; +// --------------------------------------------------------------------------- +// Skeleton entry points (slice #4, Option B). Defined in node.cpp. +// COMPILING SKELETON ONLY — the real node run-loop is M3 / Phase B. +// --------------------------------------------------------------------------- + +// One-line summary of the DGB-Scrypt network from the header-only config +// constants (no Fileconfig instantiation). +std::string network_summary(); + +// Skeleton run entry. Returns a process exit code. Phase B replaces this with +// the embedded-daemon + pool/sharechain run-loop. +int run_skeleton(); + } // namespace dgb From 5fed7b56f99acc51ce68372089654dea2fad2229 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Tue, 16 Jun 2026 13:48:33 +0000 Subject: [PATCH 2/4] dgb: link yaml-cpp on c2pool-dgb target so skeleton compiles The slice #4 skeleton reads only header constexpr constants from impl/dgb/config_{coin,pool}.hpp, but those headers include core/netaddress.hpp which pulls in . The target linked only json + Boost, so the yaml-cpp include dir was absent and the dgb smoke build failed (fatal error: yaml-cpp/yaml.h: No such file). Link yaml-cpp::yaml-cpp for its include path only. Linking the full core library instead would drag in core/web_server.o + stratum_server.o and their merged/payout/hashrate/ltc symbols (the c2pool-btc self-link tangle) and pull Phase B forward; the skeleton needs no core symbols, so the header path alone is the minimal fix. Verified: c2pool-dgb compiles, links, and runs --version/--help locally. --- src/c2pool/CMakeLists.txt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/c2pool/CMakeLists.txt b/src/c2pool/CMakeLists.txt index dc6738568..5e5f07409 100644 --- a/src/c2pool/CMakeLists.txt +++ b/src/c2pool/CMakeLists.txt @@ -161,16 +161,23 @@ target_link_libraries(c2pool-btc # binary builds + links off master. It compiles impl/dgb/node.cpp directly # rather than linking a dgb / dgb_coin / impl_dgb lib on purpose: the top-level # OBJECT-lib registration of src/impl/dgb is ci-steward's convention PR and is -# intentionally NOT raced here. The skeleton references NO core/coin/pool/ltc -# symbols (only std + the header-only dgb config), so it needs no coin lib and -# sidesteps the core/web_server self-link tangle that forces c2pool-btc to drag -# in ltc/payout/merged. Link deps stay at json + Boost until Phase B. +# intentionally NOT raced here. The skeleton reads only header constexpr +# constants from impl/dgb/config_{coin,pool}.hpp; those headers #include +# core/netaddress.hpp, which #includes . So the target needs +# yaml-cpp's INCLUDE DIR to compile — but NOT core's symbols: it never +# instantiates the Fileconfig-derived configs nor calls any core function. +# Linking the `core` library instead would drag in core/web_server.o + +# stratum_server.o, which reference merged/payout/hashrate/ltc symbols (the +# self-link tangle that forces c2pool-btc to drag in ltc/payout/merged) and +# would pull Phase B forward. We therefore add ONLY yaml-cpp (header path). +# Link deps stay at yaml-cpp + json + Boost until Phase B. add_executable(c2pool-dgb main_dgb.cpp ${CMAKE_SOURCE_DIR}/src/impl/dgb/node.cpp ) target_compile_definitions(c2pool-dgb PRIVATE C2POOL_VERSION="${C2POOL_GIT_VERSION}") target_link_libraries(c2pool-dgb + yaml-cpp::yaml-cpp # include dir only: config_{coin,pool}.hpp -> core/netaddress.hpp -> nlohmann_json::nlohmann_json ${Boost_LIBRARIES} ) From 47e9e9f3593059bcf3a86845f4fcd559c3a6ce9a Mon Sep 17 00:00:00 2001 From: frstrtr Date: Tue, 16 Jun 2026 15:48:00 +0000 Subject: [PATCH 3/4] =?UTF-8?q?dgb:=20port=20share-layer=20pillars=20(shar?= =?UTF-8?q?e=5Ftypes,=20share,=20peer)=20=E2=80=94=20namespace-only=20dive?= =?UTF-8?q?rgence=20from=20ltc;=20Formatter=20byte-parity=20with=20p2pool-?= =?UTF-8?q?merged-v36=20preserved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/impl/dgb/peer.hpp | 29 ++++ src/impl/dgb/share.hpp | 316 +++++++++++++++++++++++++++++++++++ src/impl/dgb/share_types.hpp | 196 ++++++++++++++++++++++ 3 files changed, 541 insertions(+) create mode 100644 src/impl/dgb/peer.hpp create mode 100644 src/impl/dgb/share.hpp create mode 100644 src/impl/dgb/share_types.hpp diff --git a/src/impl/dgb/peer.hpp b/src/impl/dgb/peer.hpp new file mode 100644 index 000000000..91915040b --- /dev/null +++ b/src/impl/dgb/peer.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "coin/transaction.hpp" + +#include +#include +#include +#include + +namespace dgb +{ + +struct Peer +{ + std::optional m_other_version; + std::string m_other_subversion; + uint64_t m_other_services; + uint64_t m_nonce; + std::chrono::steady_clock::time_point m_connected_at{std::chrono::steady_clock::now()}; + + std::set m_remote_txs; // hashes + // int32_t remote_remembered_txs_size = 0; + + std::map m_remembered_txs; + // int32_t remembered_txs_size = 0; + // const int32_t max_remembered_txs_size = 25000000; +}; + +}; // namespace dgb \ No newline at end of file diff --git a/src/impl/dgb/share.hpp b/src/impl/dgb/share.hpp new file mode 100644 index 000000000..d933a4812 --- /dev/null +++ b/src/impl/dgb/share.hpp @@ -0,0 +1,316 @@ +#pragma once + +#include "coin/block.hpp" +#include "share_types.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace dgb +{ + +// min_header [small_block_header_type] +// +// share_info_type: +// share_data: +// prev_hash +// coinbase +// nonce +// address[>=34] or pubkey_hash[<34] +// subsidy +// donation +// stale_info +// desired_version +// +// segwit_data [if segwit_activated] +// +// if version < 34: +// new_transaction_hashes +// transaction_hash_refs +// +// far_share_hash +// max_bits +// bits +// timestamp +// absheight +// abswork +// +// ref_merkle_link +// last_txout_nonce +// hash_link +// merkle_link + +template +struct BaseShare : chain::BaseShare +{ + coin::SmallBlockHeaderType m_min_header; + // prev_hash + BaseScript m_coinbase; // coinbase + uint32_t m_nonce; // nonce + // [x]address[>=34] or [x]pubkey_hash[<34] + uint64_t m_subsidy; // subsidy + uint16_t m_donation; // donation + dgb::StaleInfo m_stale_info; // stale_info + uint64_t m_desired_version; // desired_version + // + // [x] segwit_data [if segwit_activated] + // + uint256 m_far_share_hash; // far_share_hash + uint32_t m_max_bits; // max_bits; bitcoin_data.FloatingIntegerType() + uint32_t m_bits; // bits; bitcoin_data.FloatingIntegerType() + uint32_t m_timestamp; // timestamp + uint32_t m_absheight; // absheight + uint128 m_abswork; // abswork + + // ref_merkle_link + MerkleLink m_ref_merkle_link; + // last_txout_nonce + uint64_t m_last_txout_nonce; + // hash_link + HashLinkType m_hash_link; + // merkle_link + MerkleLink m_merkle_link; + + NetService peer_addr; // WHERE? + + BaseShare() {} + BaseShare(const uint256& hash, const uint256& prev_hash) : chain::BaseShare(hash, prev_hash) {} + +}; + +struct Share : BaseShare<17> +{ + uint160 m_pubkey_hash; + std::optional m_segwit_data; + dgb::ShareTxInfo m_tx_info; // new_transaction_hashes; transaction_hash_refs + + Share() {} + Share(const uint256& hash, const uint256& prev_hash) : BaseShare<17>(hash, prev_hash) {} + +}; + +struct NewShare : BaseShare<33> +{ + uint160 m_pubkey_hash; + std::optional m_segwit_data; + dgb::ShareTxInfo m_tx_info; // new_transaction_hashes; transaction_hash_refs + + NewShare() {} + NewShare(const uint256& hash, const uint256& prev_hash) : BaseShare<33>(hash, prev_hash) {} +}; + +namespace types +{ + +struct DataSegwitShare +{ + BaseScript m_address; // Todo (check): VarStrType + std::optional m_segwit_data; +}; + +} // namespace types + +struct SegwitMiningShare : BaseShare<34>, types::DataSegwitShare +{ + SegwitMiningShare() {} + SegwitMiningShare(const uint256& hash, const uint256& prev_hash) : BaseShare<34>(hash, prev_hash) {} +}; + +struct PaddingBugfixShare : BaseShare<35>, types::DataSegwitShare +{ + PaddingBugfixShare() {} + PaddingBugfixShare(const uint256& hash, const uint256& prev_hash) : BaseShare<35>(hash, prev_hash) {} +}; + +struct MergedMiningShare : BaseShare<36> +{ + uint160 m_pubkey_hash; + uint8_t m_pubkey_type{0}; // 0=P2PKH, 1=P2WPKH, 2=P2SH + std::optional m_segwit_data; + std::vector m_merged_addresses; // empty = none + std::vector m_merged_coinbase_info; // empty = none + uint256 m_merged_payout_hash; // zero = none + V36HashLinkType m_hash_link; // shadows BaseShare::m_hash_link + BaseScript m_message_data; // empty = none + + MergedMiningShare() {} + MergedMiningShare(const uint256& hash, const uint256& prev_hash) : BaseShare<36>(hash, prev_hash) {} +}; + +struct Formatter +{ + SHARE_FORMATTER() + { + // small_block_header_type: + READWRITE(obj->m_min_header); + // share_info_type: + READWRITE( + obj->m_prev_hash, + obj->m_coinbase, + obj->m_nonce + ); + + // Address handling — version-dependent + if constexpr (version >= 36) + { + READWRITE(obj->m_pubkey_hash); // IntType(160) + READWRITE(obj->m_pubkey_type); // IntType(8) + } + else if constexpr (version >= 34) + { + READWRITE(obj->m_address); + } + else + { + READWRITE(obj->m_pubkey_hash); // pubkey_hash + } + + // Subsidy — V36 uses VarInt, others use fixed uint64 + if constexpr (version >= 36) + { + READWRITE(VarInt(obj->m_subsidy)); + } + else + { + READWRITE(obj->m_subsidy); + } + + READWRITE( + obj->m_donation, + Using>>(obj->m_stale_info), + VarInt(obj->m_desired_version) + ); + + if constexpr (is_segwit_activated(version)) + { + READWRITE(Optional(obj->m_segwit_data, SegwitDataDefault)); + } + + // V36: merged_addresses (after segwit_data, before far_share_hash) + if constexpr (version >= 36) + { + READWRITE(obj->m_merged_addresses); + } + + if constexpr (version < 34) + { + READWRITE(obj->m_tx_info); + } + + READWRITE( + obj->m_far_share_hash, + obj->m_max_bits, + obj->m_bits, + obj->m_timestamp, + obj->m_absheight + ); + + // Abswork — V36 uses VarInt-encoded uint64, others use fixed uint128 + if constexpr (version >= 36) + { + READWRITE(Using(obj->m_abswork)); + } + else + { + READWRITE(obj->m_abswork); + } + + // V36: merged_coinbase_info + merged_payout_hash (after abswork) + if constexpr (version >= 36) + { + READWRITE(obj->m_merged_coinbase_info); + READWRITE(obj->m_merged_payout_hash); + } + + // ref_merkle_link + READWRITE( + MERKLE_LINK_SMALL(obj->m_ref_merkle_link) + ); + // last_txout_nonce + READWRITE(obj->m_last_txout_nonce); + // hash_link (V36: V36HashLinkType with extra_data; others: HashLinkType) + READWRITE(obj->m_hash_link); + // merkle_link + READWRITE( + MERKLE_LINK_SMALL(obj->m_merkle_link) + ); + + // V36: message_data (at the end) + if constexpr (version >= 36) + { + READWRITE(obj->m_message_data); + } + } +}; + +using ShareType = chain::ShareVariants; + +inline ShareType load_share(chain::RawShare& rshare, NetService peer_addr) +{ + auto stream = rshare.contents.as_stream(); + auto share = ShareType::load(rshare.type, stream); + share.ACTION({ obj->peer_addr = peer_addr; }); + return share; +} + +template +inline ShareType load_share(int64_t version, StreamType& is, NetService peer_addr) +{ + auto share = ShareType::load(version, is); + share.ACTION({ obj->peer_addr = peer_addr; }); + return share; +} + +struct ShareHasher +{ + // this used to call `GetCheapHash()` in uint256, which was later moved; the + // cheap hash function simply calls ReadLE64() however, so the end result is + // identical + size_t operator()(const uint256& hash) const + { + return hash.GetLow64(); + } +}; + +class ShareIndex : public chain::ShareIndex +{ + using base_index = chain::ShareIndex; + +public: + // Per-share fields (NOT accumulated — each share stores only its own values). + // Accumulated values are computed on-the-fly by walking m_shares[tail]. + uint288 work; // target_to_average_attempts(bits) + uint288 min_work; // target_to_average_attempts(max_bits) + + // Per-share metadata + int64_t time_seen{0}; + int32_t naughty{0}; + bool is_block_solution{false}; // pow_hash <= block_target (set during init_verify) + uint256 pow_hash; // scrypt hash, cached at reception (for block scan) + + ShareIndex() : base_index(), work(0), min_work(0) {} + + template ShareIndex(ShareT* share) : base_index(share) + { + work = chain::target_to_average_attempts(chain::bits_to_target(share->m_bits)); + min_work = chain::target_to_average_attempts(chain::bits_to_target(share->m_max_bits)); + time_seen = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + } +}; + +struct ShareChain : chain::ShareChain +{ + +}; + +} // namespace dgb + diff --git a/src/impl/dgb/share_types.hpp b/src/impl/dgb/share_types.hpp new file mode 100644 index 000000000..ef09f8e7b --- /dev/null +++ b/src/impl/dgb/share_types.hpp @@ -0,0 +1,196 @@ +#pragma once + +#include +#include +#include + +namespace dgb +{ + +const uint64_t SEGWIT_ACTIVATION_VERSION = 17; + +constexpr bool is_segwit_activated(uint64_t version) +{ + return version >= SEGWIT_ACTIVATION_VERSION; +} + +enum StaleInfo +{ + none = 0, + orphan = 253, + doa = 254 +}; + +struct MerkleLinkParams +{ + const bool allow_index; + + SER_PARAMS_OPFUNC +}; + +constexpr static MerkleLinkParams MERKLE_LINK_SMALL {.allow_index = false}; +constexpr static MerkleLinkParams MERKLE_LINK_FULL {.allow_index = true}; + +struct MerkleLink +{ + std::vector m_branch; + uint32_t m_index{0}; + + MerkleLink() { } + + template + void UnserializeMerkleLink(StreamType& s, const MerkleLinkParams& params) + { + s >> m_branch; + if (params.allow_index) + s >> m_index; + } + + template + void SerializeMerkleLink(StreamType& s, const MerkleLinkParams& params) const + { + s << m_branch; + if (params.allow_index) + s << m_index; + } + + template + inline void Serialize(StreamType& os) const + { + SerializeMerkleLink(os, os.GetParams()); + } + + template + inline void Unserialize(StreamType& is) + { + UnserializeMerkleLink(is, is.GetParams()); + } +}; + +struct SegwitData +{ + MerkleLink m_txid_merkle_link; + uint256 m_wtxid_merkle_root; + + SegwitData() {} + SegwitData(MerkleLink txid_merkle_link, uint256 wtxid) : m_txid_merkle_link(txid_merkle_link), m_wtxid_merkle_root(wtxid) { } + + C2POOL_SERIALIZE_METHODS(SegwitData) { READWRITE(MERKLE_LINK_SMALL(obj.m_txid_merkle_link), obj.m_wtxid_merkle_root); } +}; + +struct SegwitDataDefault +{ + static SegwitData get() + { + // Sentinel must match p2pool's PossiblyNoneType sentinel: + // dict(txid_merkle_link=dict(branch=[], index=0), wtxid_merkle_root=0) + // Using all-0xff caused p2pool to interpret "None" as a valid wtxid root, + // producing different witness commitment → different coinbase txid. + return SegwitData{{}, uint256()}; // zero = None sentinel + } +}; + +struct TxHashRefs +{ + uint64_t m_share_count; + uint64_t m_tx_count; + + TxHashRefs() = default; + TxHashRefs(uint64_t share, uint64_t tx) : m_share_count(share), m_tx_count(tx) {} + + C2POOL_SERIALIZE_METHODS(TxHashRefs) { READWRITE(VarInt(obj.m_share_count), VarInt(obj.m_tx_count)); } +}; + +struct ShareTxInfo +{ + std::vector m_new_transaction_hashes; + std::vector m_transaction_hash_refs; //pack.ListType(pack.VarIntType(), 2)), # pairs of share_count, tx_count + + ShareTxInfo() = default; + + ShareTxInfo(const auto& new_tx_hashes, const auto& tx_hash_refs) + : m_new_transaction_hashes(new_tx_hashes), m_transaction_hash_refs(tx_hash_refs) { } + + C2POOL_SERIALIZE_METHODS(ShareTxInfo) { READWRITE(obj.m_new_transaction_hashes, obj.m_transaction_hash_refs); } +}; + +struct HashLinkType +{ + FixedStrType<32> m_state; //pack.FixedStrType(32) + // FixedStrType<0> m_extra_data; //pack.FixedStrType(0) # bit of a hack, but since the donation script is at the end, const_ending is long enough to always make this empty + uint64_t m_length; //pack.VarIntType() + + C2POOL_SERIALIZE_METHODS(HashLinkType) { READWRITE(obj.m_state, /*obj.m_extra_data,*/ VarInt(obj.m_length)); } +}; + +// V36 hash link (DGB-Scrypt; share format parity with p2pool-merged-v36) — extra_data becomes VarStr (was FixedStr(0) pre-V36) +struct V36HashLinkType +{ + FixedStrType<32> m_state; + BaseScript m_extra_data; // VarStr in V36 + uint64_t m_length; + + C2POOL_SERIALIZE_METHODS(V36HashLinkType) { READWRITE(obj.m_state, obj.m_extra_data, VarInt(obj.m_length)); } +}; + +// V36 merged mining: per-chain address entry +struct MergedAddressEntry +{ + uint32_t m_chain_id; + BaseScript m_script; + + C2POOL_SERIALIZE_METHODS(MergedAddressEntry) { READWRITE(obj.m_chain_id, obj.m_script); } +}; + +// V36 merged mining: per-chain coinbase verification entry +struct MergedCoinbaseEntry +{ + uint32_t m_chain_id; + uint64_t m_coinbase_value; + uint32_t m_block_height; + FixedStrType<80> m_block_header; + MerkleLink m_coinbase_merkle_link; + BaseScript m_coinbase_script; // V36: actual scriptSig (allows custom tags + THE state_root) + + template + void Serialize(StreamType& os) const + { + ::Serialize(os, m_chain_id); + ::Serialize(os, Using(m_coinbase_value)); + ::Serialize(os, Using(m_block_height)); + ::Serialize(os, m_block_header); + ParamPackStream pstream{MERKLE_LINK_SMALL, os}; + ::Serialize(pstream, m_coinbase_merkle_link); + ::Serialize(os, m_coinbase_script); + } + + template + void Unserialize(StreamType& is) + { + ::Unserialize(is, m_chain_id); + ::Unserialize(is, Using(m_coinbase_value)); + ::Unserialize(is, Using(m_block_height)); + ::Unserialize(is, m_block_header); + ParamPackStream pstream{MERKLE_LINK_SMALL, is}; + ::Unserialize(pstream, m_coinbase_merkle_link); + ::Unserialize(is, m_coinbase_script); + } +}; + +// V36: abswork is VarInt-encoded on the wire but stored as uint128 +struct AbsworkV36Format +{ + template + static void Write(StreamType& os, const uint128& value) + { + WriteCompactSize(os, value.GetLow64()); + } + + template + static void Read(StreamType& is, uint128& value) + { + value = uint128(ReadCompactSize(is, false)); + } +}; + +} // namespace dgb From 8da2172f8f1860e5bd0f7b91f2fb145cc83febfa Mon Sep 17 00:00:00 2001 From: frstrtr Date: Tue, 16 Jun 2026 15:53:43 +0000 Subject: [PATCH 4/4] =?UTF-8?q?dgb:=20port=20pool=20wire-message=20layer?= =?UTF-8?q?=20(messages,=20share=5Fmessages)=20=E2=80=94=20Scrypt-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pool/share layer slice 1 on family-1 foundation, stacked on share-layer pillars (#113). Namespace-only divergence from src/impl/ltc; the SHA256/ HMAC-SHA256/ECDSA in share_messages is the PoW-algo-independent p2pool message-signing layer (not Scrypt block PoW). Wire/message format byte- parity with p2pool-merged-v36 preserved. Unblocks share_check/share_tracker port (next slice). --- src/impl/dgb/messages.hpp | 193 +++++++++++ src/impl/dgb/share_messages.hpp | 581 ++++++++++++++++++++++++++++++++ 2 files changed, 774 insertions(+) create mode 100644 src/impl/dgb/messages.hpp create mode 100644 src/impl/dgb/share_messages.hpp diff --git a/src/impl/dgb/messages.hpp b/src/impl/dgb/messages.hpp new file mode 100644 index 000000000..edf78cb83 --- /dev/null +++ b/src/impl/dgb/messages.hpp @@ -0,0 +1,193 @@ +#pragma once +// V36 DGB-Scrypt pool wire-message layer — namespace-only divergence from +// src/impl/ltc/messages.hpp; message format byte-parity with p2pool-merged-v36. + +#include "coin/block.hpp" +#include "coin/transaction.hpp" + +#include + +#include +#include +#include + +namespace dgb +{ + +// message_version +BEGIN_MESSAGE(version) + MESSAGE_FIELDS + ( + (uint32_t, m_version), + (uint64_t, m_services), + (addr_t , m_addr_to), + (addr_t, m_addr_from), + (uint64_t, m_nonce), + (std::string, m_subversion), + (uint32_t, m_mode), //# always 1 for legacy compatibility + (uint256, m_best_share) + ) + { + READWRITE(obj.m_version, obj.m_services, obj.m_addr_to, obj.m_addr_from, obj.m_nonce, obj.m_subversion, obj.m_mode, obj.m_best_share); + } +END_MESSAGE() + +// message_ping +BEGIN_MESSAGE(ping) + WITHOUT_MESSAGE_FIELDS() { } +END_MESSAGE() + +// message_addrme +BEGIN_MESSAGE(addrme) + MESSAGE_FIELDS + ( + (uint16_t, m_port) + ) + { + READWRITE(obj.m_port); + } +END_MESSAGE() + +// message_getaddrs +BEGIN_MESSAGE(getaddrs) + MESSAGE_FIELDS + ( + (uint32_t, m_count) + ) + { + READWRITE(obj.m_count); + } +END_MESSAGE() + +// message_addrs +BEGIN_MESSAGE(addrs) + MESSAGE_FIELDS + ( + (std::vector, m_addrs) + ) + { + READWRITE(obj.m_addrs); + } +END_MESSAGE() + +// message_shares +BEGIN_MESSAGE(shares) + MESSAGE_FIELDS + ( + (std::vector, m_shares) + ) + { + READWRITE(obj.m_shares); + } +END_MESSAGE() + +// message_sharereq +BEGIN_MESSAGE(sharereq) + MESSAGE_FIELDS + ( + (uint256, m_id), + (std::vector, m_hashes), + (uint64_t, m_parents), + (std::vector, m_stops) + ) + { + READWRITE(obj.m_id, obj.m_hashes, VarInt(obj.m_parents), obj.m_stops); + } +END_MESSAGE() + +enum ShareReplyResult +{ + good = 0, + too_long = 1, + unk2 = 2, + unk3 = 3, + unk4 = 4, + unk5 = 5, + unk6 = 6 +}; + +// message_sharereply +BEGIN_MESSAGE(sharereply) + MESSAGE_FIELDS + ( + (uint256, m_id), + (ShareReplyResult, m_result), + (std::vector, m_shares) + ) + { + READWRITE(obj.m_id, Using>(obj.m_result), obj.m_shares); + } +END_MESSAGE() + +// message_bestblock +BEGIN_MESSAGE(bestblock) + MESSAGE_FIELDS + ( + (coin::BlockHeaderType, m_header) + ) + { + READWRITE(obj.m_header); + } +END_MESSAGE() + +// message_have_tx +BEGIN_MESSAGE(have_tx) + MESSAGE_FIELDS + ( + (std::vector, m_tx_hashes) + ) + { + READWRITE(obj.m_tx_hashes); + } +END_MESSAGE() + +// message_losing_tx +BEGIN_MESSAGE(losing_tx) + MESSAGE_FIELDS + ( + (std::vector, m_tx_hashes) + ) + { + READWRITE(obj.m_tx_hashes); + } +END_MESSAGE() + +// message_forget_tx +BEGIN_MESSAGE(forget_tx) + MESSAGE_FIELDS + ( + (std::vector, m_tx_hashes) + ) + { + READWRITE(obj.m_tx_hashes); + } +END_MESSAGE() + +// message_remember_tx +BEGIN_MESSAGE(remember_tx) + MESSAGE_FIELDS + ( + (std::vector, m_tx_hashes), + (std::vector, m_txs) + ) + { + READWRITE(obj.m_tx_hashes, coin::TX_WITH_WITNESS(obj.m_txs)); + } +END_MESSAGE() + +using Handler = MessageHandler< + message_ping, + message_addrme, + message_getaddrs, + message_addrs, + message_shares, + message_sharereq, + message_sharereply, + message_bestblock, + message_have_tx, + message_losing_tx, + message_forget_tx, + message_remember_tx +>; + +} // namespace dgb diff --git a/src/impl/dgb/share_messages.hpp b/src/impl/dgb/share_messages.hpp new file mode 100644 index 000000000..7e2719dee --- /dev/null +++ b/src/impl/dgb/share_messages.hpp @@ -0,0 +1,581 @@ +// share_messages.hpp — V36 share-embedded messaging: decryption + validation +// +// Port of p2pool/share_messages.py (consensus-critical portions). +// Messages are embedded in V36 shares' message_data field, included +// in the ref_hash computation (PoW-protected). +// +// Validation path for incoming shares: +// 1. If message_data is empty → valid (no messages) +// 2. Decrypt outer envelope using DONATION_AUTHORITY_PUBKEYS +// 3. If decryption fails (MAC mismatch) → REJECT share +// 4. Parse inner envelope: version, flags, msg_count, messages +// 5. If inner envelope is malformed or empty → REJECT share +// 6. Verify ECDSA signatures via libsecp256k1 + +#pragma once +// DGB-Scrypt note: the SHA256/HMAC-SHA256/ECDSA here are the p2pool message-signing +// layer (PoW-algo-independent) — identical to ltc; NOT the Scrypt block PoW. + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace dgb { + +// ============================================================================ +// Authority public keys (compressed secp256k1) +// From COMBINED_DONATION_REDEEM_SCRIPT: +// OP_1 PUSH33 PUSH33 OP_2 OP_CHECKMULTISIG +// ============================================================================ + +using AuthorityPubkey = std::array; + +inline const AuthorityPubkey& DONATION_PUBKEY_FORRESTV() +{ + static const AuthorityPubkey k = { + 0x03, 0xff, 0xd0, 0x3d, 0xe4, 0x4a, 0x6e, 0x11, + 0xb9, 0x91, 0x7f, 0x3a, 0x29, 0xf9, 0x44, 0x32, + 0x83, 0xd9, 0x87, 0x1c, 0x9d, 0x74, 0x3e, 0xf3, + 0x0d, 0x5e, 0xdd, 0xcd, 0x37, 0x09, 0x4b, 0x64, + 0xd1 + }; + return k; +} + +inline const AuthorityPubkey& DONATION_PUBKEY_MAINTAINER() +{ + static const AuthorityPubkey k = { + 0x02, 0xfe, 0x65, 0x78, 0xf8, 0x02, 0x1a, 0x7d, + 0x46, 0x67, 0x87, 0x82, 0x7b, 0x3f, 0x26, 0x43, + 0x7a, 0xef, 0x88, 0x27, 0x9e, 0xf3, 0x80, 0xaf, + 0x32, 0x6f, 0x87, 0xec, 0x36, 0x26, 0x33, 0x29, + 0x3a + }; + return k; +} + +inline const std::array& DONATION_AUTHORITY_PUBKEYS() +{ + static const std::array keys = { + &DONATION_PUBKEY_FORRESTV(), + &DONATION_PUBKEY_MAINTAINER(), + }; + return keys; +} + +// ============================================================================ +// Constants +// ============================================================================ + +// Message types +constexpr uint8_t MSG_NODE_STATUS = 0x01; +constexpr uint8_t MSG_MINER_MESSAGE = 0x02; +constexpr uint8_t MSG_POOL_ANNOUNCE = 0x03; +constexpr uint8_t MSG_VERSION_SIGNAL = 0x04; +constexpr uint8_t MSG_MERGED_STATUS = 0x05; +constexpr uint8_t MSG_EMERGENCY = 0x10; +constexpr uint8_t MSG_TRANSITION_SIGNAL = 0x20; + +// Flags +constexpr uint8_t FLAG_HAS_SIGNATURE = 0x01; +constexpr uint8_t FLAG_BROADCAST = 0x02; +constexpr uint8_t FLAG_PERSISTENT = 0x04; +constexpr uint8_t FLAG_PROTOCOL_AUTHORITY = 0x08; + +// Limits +constexpr size_t MAX_MESSAGE_PAYLOAD = 220; +constexpr size_t MAX_MESSAGES_PER_SHARE = 3; +constexpr size_t MAX_TOTAL_MESSAGE_BYTES = 512; + +// Encryption envelope +constexpr uint8_t ENCRYPTED_ENVELOPE_VERSION = 0x01; +constexpr size_t ENCRYPTION_NONCE_SIZE = 16; +constexpr size_t ENCRYPTION_MAC_SIZE = 32; +constexpr size_t ENCRYPTION_HEADER_SIZE = 1 + ENCRYPTION_NONCE_SIZE + ENCRYPTION_MAC_SIZE; // 49 + +// ============================================================================ +// Parsed message (lightweight — just what we need for validation) +// ============================================================================ + +struct ShareMessage +{ + uint8_t msg_type{0}; + uint8_t flags{0}; + uint8_t wire_flags{0}; // original flags for hash + uint32_t timestamp{0}; + std::vector payload; + std::array signing_id{}; + std::vector signature; + + bool has_signature() const { return (flags & FLAG_HAS_SIGNATURE) != 0; } + + // Unpack one message from data at offset. Returns new offset or nullopt on failure. + static std::optional unpack(const unsigned char* data, size_t len, + size_t offset, ShareMessage& out) + { + if (len - offset < 8) return std::nullopt; + + std::memcpy(&out.msg_type, data + offset, 1); + std::memcpy(&out.wire_flags, data + offset + 1, 1); + out.flags = out.wire_flags & ~FLAG_PROTOCOL_AUTHORITY; + + uint32_t ts; + std::memcpy(&ts, data + offset + 2, 4); // LE + out.timestamp = ts; + + uint16_t payload_len; + std::memcpy(&payload_len, data + offset + 6, 2); // LE + offset += 8; + + if (payload_len > MAX_MESSAGE_PAYLOAD) return std::nullopt; + if (len - offset < payload_len) return std::nullopt; + out.payload.assign(data + offset, data + offset + payload_len); + offset += payload_len; + + // signing_id (20 bytes) + if (len - offset < 20) return std::nullopt; + std::memcpy(out.signing_id.data(), data + offset, 20); + offset += 20; + + // sig_len + signature + if (len - offset < 1) return std::nullopt; + uint8_t sig_len = data[offset++]; + if (len - offset < sig_len) return std::nullopt; + out.signature.assign(data + offset, data + offset + sig_len); + offset += sig_len; + + return offset; + } +}; + +// ============================================================================ +// secp256k1 context (singleton, thread-safe after init) +// ============================================================================ + +inline const secp256k1_context* get_secp256k1_context() +{ + static const secp256k1_context* ctx = + secp256k1_context_create(SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN); + return ctx; +} + +// ============================================================================ +// Crypto helpers +// ============================================================================ + +// HMAC-SHA256(key, data) → 32 bytes +inline std::array hmac_sha256( + const unsigned char* key, size_t keylen, + const unsigned char* data, size_t datalen) +{ + std::array result; + CHMAC_SHA256(key, keylen).Write(data, datalen).Finalize(result.data()); + return result; +} + +// Counter-mode SHA256 stream generator +inline void generate_stream(const unsigned char* enc_key, + unsigned char* out, size_t length) +{ + size_t produced = 0; + uint32_t counter = 0; + while (produced < length) + { + unsigned char block_input[36]; // 32 (key) + 4 (counter LE) + std::memcpy(block_input, enc_key, 32); + std::memcpy(block_input + 32, &counter, 4); // LE on little-endian host + + unsigned char block[32]; + CSHA256().Write(block_input, 36).Finalize(block); + + size_t copy = std::min(32, length - produced); + std::memcpy(out + produced, block, copy); + produced += copy; + ++counter; + } +} + +// Double-SHA256 of message content for ECDSA signing/verification. +// Matches Python: SHA256(SHA256(pack(' compute_message_hash( + uint8_t msg_type, uint8_t wire_flags, uint32_t timestamp, + const unsigned char* payload, size_t payload_len) +{ + unsigned char header[6]; + header[0] = msg_type; + header[1] = wire_flags; + std::memcpy(header + 2, ×tamp, 4); // LE + + unsigned char first[32]; + CSHA256().Write(header, 6).Write(payload, payload_len).Finalize(first); + + std::array result; + CSHA256().Write(first, 32).Finalize(result.data()); + return result; +} + +// Verify ECDSA signature against a compressed secp256k1 public key. +// pubkey_compressed: 33 bytes (0x02/0x03 prefix) +// msghash32: 32-byte double-SHA256 (from compute_message_hash) +// sig_der: DER-encoded ECDSA signature +// Returns true if signature is valid. +inline bool ecdsa_verify(const unsigned char* pubkey_compressed, size_t pubkey_len, + const unsigned char* msghash32, + const unsigned char* sig_der, size_t sig_len) +{ + const auto* ctx = get_secp256k1_context(); + + secp256k1_pubkey pubkey; + if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, pubkey_compressed, pubkey_len)) + return false; + + secp256k1_ecdsa_signature sig; + if (!secp256k1_ecdsa_signature_parse_der(ctx, &sig, sig_der, sig_len)) + return false; + + // Normalize to lower-S form (secp256k1_ecdsa_verify requires this) + secp256k1_ecdsa_signature_normalize(ctx, &sig, &sig); + + return secp256k1_ecdsa_verify(ctx, &sig, msghash32, &pubkey) == 1; +} + +// ============================================================================ +// Decryption result +// ============================================================================ + +struct DecryptResult +{ + std::vector inner_data; + const AuthorityPubkey* authority_pubkey{nullptr}; // which key succeeded +}; + +// Decrypt encrypted envelope. Returns nullopt if all authority keys fail MAC. +inline std::optional decrypt_message_data( + const unsigned char* data, size_t len) +{ + if (len < ENCRYPTION_HEADER_SIZE + 1) return std::nullopt; + + if (data[0] != ENCRYPTED_ENVELOPE_VERSION) return std::nullopt; + + const unsigned char* nonce = data + 1; + const unsigned char* mac_recv = data + 1 + ENCRYPTION_NONCE_SIZE; + const unsigned char* ciphertext = data + ENCRYPTION_HEADER_SIZE; + size_t ct_len = len - ENCRYPTION_HEADER_SIZE; + + if (ct_len == 0) return std::nullopt; + + for (const auto* pubkey_ptr : DONATION_AUTHORITY_PUBKEYS()) + { + // enc_key = HMAC-SHA256(pubkey, nonce) + auto enc_key = hmac_sha256( + pubkey_ptr->data(), pubkey_ptr->size(), nonce, ENCRYPTION_NONCE_SIZE); + + // MAC = HMAC-SHA256(enc_key, ciphertext) + auto mac_computed = hmac_sha256( + enc_key.data(), enc_key.size(), ciphertext, ct_len); + + // Constant-time compare + unsigned char diff = 0; + for (size_t i = 0; i < 32; ++i) diff |= mac_computed[i] ^ mac_recv[i]; + if (diff != 0) continue; // Wrong key + + // MAC matches — decrypt (XOR with stream) + DecryptResult result; + result.inner_data.resize(ct_len); + generate_stream(enc_key.data(), result.inner_data.data(), ct_len); + for (size_t i = 0; i < ct_len; ++i) + result.inner_data[i] ^= ciphertext[i]; + result.authority_pubkey = pubkey_ptr; + return result; + } + + return std::nullopt; +} + +// ============================================================================ +// Unpack result +// ============================================================================ + +struct UnpackResult +{ + std::vector messages; + const AuthorityPubkey* authority_pubkey{nullptr}; + bool decrypted{false}; +}; + +// Main entry point: decrypt + parse inner envelope. +// Returns empty result on failure; caller checks fields. +inline UnpackResult unpack_share_messages(const unsigned char* data, size_t len) +{ + UnpackResult result; + + if (!data || len < ENCRYPTION_HEADER_SIZE + 4) + return result; + + auto dec = decrypt_message_data(data, len); + if (!dec.has_value()) + return result; // Decryption failed → signing_key_info = nullptr + + result.decrypted = true; + result.authority_pubkey = dec->authority_pubkey; + + const auto& inner = dec->inner_data; + if (inner.size() < 4) + return result; + + uint8_t inner_version = inner[0]; + uint8_t inner_flags = inner[1]; + uint8_t msg_count = inner[2]; + uint8_t ann_len = inner[3]; + size_t offset = 4; + + if (inner_version != 1) + return result; // Unknown version — skip gracefully + + // Skip signing key announcement if present + if ((inner_flags & 0x01) && ann_len > 0) + offset += ann_len; + + if (offset > inner.size()) + return result; + + // Cap message count + if (msg_count > MAX_MESSAGES_PER_SHARE) + msg_count = MAX_MESSAGES_PER_SHARE; + + for (uint8_t i = 0; i < msg_count; ++i) + { + ShareMessage msg; + auto new_offset = ShareMessage::unpack(inner.data(), inner.size(), offset, msg); + if (!new_offset.has_value()) break; + result.messages.push_back(std::move(msg)); + offset = *new_offset; + } + + return result; +} + +// Convenience: validate message_data from a share. +// Returns empty string on success; returns error reason on failure. +// Empty message_data is always valid. +inline std::string validate_message_data(const std::vector& message_data) +{ + if (message_data.empty()) + return {}; // No messages — always valid + + auto result = unpack_share_messages(message_data.data(), message_data.size()); + + if (!result.decrypted) + return "message_data failed decryption against all " + "COMBINED_DONATION_SCRIPT authority keys"; + + if (result.authority_pubkey == nullptr) + return "message_data decrypted but authority_pubkey unknown"; + + // Check authority_pubkey is one we recognise + bool known = false; + for (const auto* pk : DONATION_AUTHORITY_PUBKEYS()) + { + if (pk == result.authority_pubkey) { known = true; break; } + } + if (!known) + return "message_data authority_pubkey not in COMBINED_DONATION_SCRIPT"; + + if (result.messages.empty()) + return "message_data decrypted but contains no valid messages"; + + // Verify each message has a valid ECDSA signature from the authority key + // that encrypted the envelope. This is defense-in-depth on top of the + // MAC-based decryption (which already proves authority created the envelope). + for (const auto& msg : result.messages) + { + if (!msg.has_signature() || msg.signature.empty()) + return "message contains unsigned message (type 0x" + + std::to_string(msg.msg_type) + ") inside encrypted envelope"; + + auto msg_hash = compute_message_hash( + msg.msg_type, msg.wire_flags, msg.timestamp, + msg.payload.data(), msg.payload.size()); + + if (!ecdsa_verify(result.authority_pubkey->data(), result.authority_pubkey->size(), + msg_hash.data(), msg.signature.data(), msg.signature.size())) + return "message ECDSA signature verification failed (type 0x" + + std::to_string(msg.msg_type) + ")"; + } + + return {}; // All checks passed +} + +// ============================================================================ +// Message creation: signing, packing, encryption +// ============================================================================ + +// ECDSA sign a 32-byte hash with a 32-byte private key. +// Returns DER-encoded signature, empty on failure. +inline std::vector ecdsa_sign( + const unsigned char* msghash32, + const unsigned char* seckey32) +{ + const auto* ctx = get_secp256k1_context(); + + secp256k1_ecdsa_signature sig; + if (!secp256k1_ecdsa_sign(ctx, &sig, msghash32, seckey32, nullptr, nullptr)) + return {}; + + unsigned char der[72]; + size_t der_len = sizeof(der); + if (!secp256k1_ecdsa_signature_serialize_der(ctx, der, &der_len, &sig)) + return {}; + + return {der, der + der_len}; +} + +// HASH160 = RIPEMD160(SHA256(data)) — standard Bitcoin hash160. +inline std::array hash160( + const unsigned char* data, size_t len) +{ + unsigned char sha256_buf[32]; + CSHA256().Write(data, len).Finalize(sha256_buf); + + std::array result; + CRIPEMD160().Write(sha256_buf, 32).Finalize(result.data()); + return result; +} + +// Serialize a ShareMessage to wire format. +// Wire: [type:1][flags:1][timestamp:4 LE][payload_len:2 LE][payload:N] +// [signing_id:20][sig_len:1][signature:M] +inline std::vector pack_message(const ShareMessage& msg) +{ + std::vector buf; + buf.reserve(8 + msg.payload.size() + 20 + 1 + msg.signature.size()); + + buf.push_back(msg.msg_type); + buf.push_back(msg.wire_flags); + + uint32_t ts = msg.timestamp; + buf.push_back(static_cast(ts)); + buf.push_back(static_cast(ts >> 8)); + buf.push_back(static_cast(ts >> 16)); + buf.push_back(static_cast(ts >> 24)); + + uint16_t pl = static_cast(msg.payload.size()); + buf.push_back(static_cast(pl)); + buf.push_back(static_cast(pl >> 8)); + + buf.insert(buf.end(), msg.payload.begin(), msg.payload.end()); + buf.insert(buf.end(), msg.signing_id.begin(), msg.signing_id.end()); + + buf.push_back(static_cast(msg.signature.size())); + buf.insert(buf.end(), msg.signature.begin(), msg.signature.end()); + + return buf; +} + +// Encrypt inner envelope data with an authority public key. +// Returns encrypted blob: [0x01][nonce:16][mac:32][ciphertext:N] +inline std::vector encrypt_message_envelope( + const std::vector& inner, + const AuthorityPubkey& authority_pubkey) +{ + // 16-byte random nonce + std::array nonce; + std::random_device rd; + for (size_t i = 0; i < ENCRYPTION_NONCE_SIZE; i += 4) + { + auto val = rd(); + auto bytes = std::min(4, ENCRYPTION_NONCE_SIZE - i); + std::memcpy(nonce.data() + i, &val, bytes); + } + + // enc_key = HMAC-SHA256(authority_pubkey, nonce) + auto enc_key = hmac_sha256( + authority_pubkey.data(), authority_pubkey.size(), + nonce.data(), nonce.size()); + + // stream = counter-mode SHA256 + std::vector stream(inner.size()); + generate_stream(enc_key.data(), stream.data(), inner.size()); + + // ciphertext = inner XOR stream + std::vector ciphertext(inner.size()); + for (size_t i = 0; i < inner.size(); ++i) + ciphertext[i] = inner[i] ^ stream[i]; + + // mac = HMAC-SHA256(enc_key, ciphertext) + auto mac = hmac_sha256( + enc_key.data(), enc_key.size(), + ciphertext.data(), ciphertext.size()); + + // Assemble: [0x01][nonce:16][mac:32][ciphertext] + std::vector result; + result.reserve(1 + ENCRYPTION_NONCE_SIZE + ENCRYPTION_MAC_SIZE + ciphertext.size()); + result.push_back(ENCRYPTED_ENVELOPE_VERSION); + result.insert(result.end(), nonce.begin(), nonce.end()); + result.insert(result.end(), mac.begin(), mac.end()); + result.insert(result.end(), ciphertext.begin(), ciphertext.end()); + return result; +} + +// Create encrypted message_data blob for embedding in a V36 share. +// +// seckey: 32-byte private key +// authority_pubkey: 33-byte compressed pubkey corresponding to seckey +// messages: ShareMessages with msg_type, wire_flags, timestamp, payload set. +// signature and signing_id are computed and filled in. +// +// Returns the complete encrypted message_data blob, or empty on failure. +inline std::vector create_message_data( + const unsigned char* seckey, + const AuthorityPubkey& authority_pubkey, + std::vector& messages) +{ + if (messages.empty() || messages.size() > MAX_MESSAGES_PER_SHARE) + return {}; + + // Compute signing_id = HASH160(authority_pubkey) + auto signing_id = hash160(authority_pubkey.data(), authority_pubkey.size()); + + // Sign each message and serialize + std::vector packed_messages; + for (auto& msg : messages) + { + msg.signing_id = signing_id; + + auto msg_hash = compute_message_hash( + msg.msg_type, msg.wire_flags, msg.timestamp, + msg.payload.data(), msg.payload.size()); + + msg.signature = ecdsa_sign(msg_hash.data(), seckey); + if (msg.signature.empty()) + return {}; + + auto packed = pack_message(msg); + packed_messages.insert(packed_messages.end(), packed.begin(), packed.end()); + } + + // Inner envelope: [version=1][flags=0][msg_count][reserved=0] + packed_messages + std::vector inner; + inner.reserve(4 + packed_messages.size()); + inner.push_back(0x01); + inner.push_back(0x00); + inner.push_back(static_cast(messages.size())); + inner.push_back(0x00); + inner.insert(inner.end(), packed_messages.begin(), packed_messages.end()); + + if (inner.size() > MAX_TOTAL_MESSAGE_BYTES) + return {}; + + return encrypt_message_envelope(inner, authority_pubkey); +} + +} // namespace dgb