Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ jobs:
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
dgb_coin_node_seam_test dgb_block_broadcast_test \
v37_test \
-j$(nproc)

Expand Down Expand Up @@ -203,6 +204,7 @@ jobs:
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
dgb_coin_node_seam_test dgb_block_broadcast_test \
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
v37_test \
-j$(nproc)
Expand Down
106 changes: 106 additions & 0 deletions src/impl/dgb/coin/block_broadcast.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#pragma once
// ---------------------------------------------------------------------------
// dgb::coin::broadcast_won_block -- the DGB dual-path won-block dispatcher.
//
// 1:1 contract mirror of bch::coin::EmbeddedDaemon::broadcast_won_block
// (src/impl/bch/coin/embedded_daemon.hpp @90a35536): on a won block it fires
// BOTH broadcast sinks --
// PRIMARY : embedded P2P relay (fastest propagation), supplied by the
// run-loop as a sink fn (DGB has no submit_block_p2p_raw on
// coin::Node yet -- the EmbeddedDaemon/NodeP2P port binds this).
// FALLBACK : external digibyted submitblock via the CoinNode seam
// (core::coin::ICoinNode::submit_block_hex) -- fired ALWAYS, per
// the broadcaster-gate dual-path rule, NOT a P2P-only path with
// RPC as a catch. A duplicate on the RPC leg after a P2P accept
// still proves both paths reached the node; landed_first records
// which won the race. ignore_failure=true so an already-have on
// the fallback never masks the P2P win.
//
// This is the sink the DGB pool node wires tracker().m_on_block_found to so an
// in-operation win emits immediately (the remaining #82 run-loop slice; the
// pool-node wiring mirrors bch @9fed4955). Decoupled from the (not-yet-ported)
// DGB EmbeddedDaemon by taking the P2P sink as a std::function and the seam as
// an ICoinNode*, so it is build-verifiable now and the run-loop binds the live
// objects when NodeP2P lands.
//
// DGB-Scrypt is a STANDALONE parent in the V36 default build -- no merged-
// coinbase leg (DOGE aux is -DAUX_DOGE=ON stretch, a separate parent path).
// p2pool-merged-v36 surface: NONE -- block dispatch only, no PoW hash, share
// format, coinbase commitment, or PPLNS math is touched. Per-coin isolation:
// src/impl/dgb/coin/ only.
// ---------------------------------------------------------------------------

#include <cstdint>
#include <functional>
#include <string>
#include <vector>

#include <core/coin/node_iface.hpp>
#include <core/log.hpp>

namespace dgb
{
namespace coin
{

// Embedded P2P relay sink: the run-loop binds this to NodeP2P::submit_block_p2p_raw
// once the embedded daemon is stood up. Empty == no embedded P2P sink present.
using P2pRelaySink = std::function<void(const std::vector<unsigned char>&)>;

// Outcome of a won-block broadcast: which of the two sinks fired and whether
// the external node acked. P2P is primary; the submitblock RPC fallback fires
// ALWAYS (dual-path rule). landed_first records which path won the race.
struct BlockBroadcast
{
bool p2p_sent = false; // embedded P2P relay issued (sink present)
bool rpc_ok = false; // submitblock returned ok OR duplicate
const char* landed_first = "none"; // "p2p" | "rpc" | "none"
bool any() const { return p2p_sent || rpc_ok; }
};

// Fire a won block down BOTH broadcast paths. `block_bytes` is the pre-
// serialized block blob the embedded P2P relay sends; `block_hex` is the same
// block hex for the external submitblock fallback. `p2p_relay` may be empty
// (no embedded sink yet); `seam` may be null or RPC-less -- each leg is guarded
// so the dispatcher never throws or dereferences a null sink.
inline BlockBroadcast broadcast_won_block(const P2pRelaySink& p2p_relay,
core::coin::ICoinNode* seam,
const std::vector<unsigned char>& block_bytes,
const std::string& block_hex)
{
BlockBroadcast r;

// PRIMARY: embedded P2P relay (fastest propagation).
if (p2p_relay) {
p2p_relay(block_bytes);
r.p2p_sent = true;
r.landed_first = "p2p";
LOG_INFO << "[EMB-DGB] won-block P2P relay issued (" << block_bytes.size()
<< " bytes) -- primary path.";
} else {
LOG_WARNING << "[EMB-DGB] won-block: no embedded P2P sink; relying on RPC fallback.";
}

// FALLBACK (always fired): external digibyted submitblock. A duplicate here
// after a P2P accept is success, not failure -- ignore_failure=true so a
// duplicate/already-have does not mask the P2P win.
if (seam && seam->has_rpc()) {
r.rpc_ok = seam->submit_block_hex(block_hex, /*ignore_failure=*/true);
if (r.rpc_ok && !r.p2p_sent) r.landed_first = "rpc";
LOG_INFO << "[EMB-DGB] won-block submitblock RPC fallback "
<< (r.rpc_ok ? "ok/duplicate" : "no-ack") << ".";
} else {
LOG_WARNING << "[EMB-DGB] won-block: no external digibyted-RPC fallback sink wired.";
}

if (!r.any())
LOG_ERROR << "[EMB-DGB] won-block had NEITHER broadcast sink -- block NOT relayed!";
else
LOG_INFO << "[EMB-DGB] won-block broadcast: p2p=" << (r.p2p_sent ? "sent" : "off")
<< " rpc=" << (r.rpc_ok ? "ok" : "off")
<< " landed_first=" << r.landed_first << ".";
return r;
}

} // namespace coin
} // namespace dgb
25 changes: 25 additions & 0 deletions src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,29 @@ if (BUILD_TESTING AND GTest_FOUND)
GTest::gtest_main GTest::gtest nlohmann_json::nlohmann_json)
gtest_add_tests(${dgb_guard} "" AUTO)
endforeach()
# --- #82 broadcaster-gate: CoinNode submit/seam contract -----------------
# Links the dgb OBJECT lib (coin_node.cpp) like dgb_share_test so the live
# CoinNode::submit_block_hex !m_rpc guard + embedded WorkView slice are
# build- and run-verified. Pins the external-digibyted RPC fallback half of
# the won-block dispatch (web_server -> ICoinNode::submit_block_hex).
add_executable(dgb_coin_node_seam_test coin_node_seam_test.cpp)
target_link_libraries(dgb_coin_node_seam_test PRIVATE
GTest::gtest_main GTest::gtest
core dgb
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
dgb_coin pool sharechain)
gtest_add_tests(dgb_coin_node_seam_test "" AUTO)

# --- #82 dual-path won-block broadcaster contract ----------------------
# Header-only contract test for coin/block_broadcast.hpp (the dispatcher the
# run-loop wires tracker().m_on_block_found to). Links core for the ICoinNode
# seam + LOG_* symbols; needs no dgb OBJECT lib (uses a fake ICoinNode).
add_executable(dgb_block_broadcast_test block_broadcast_test.cpp)
target_link_libraries(dgb_block_broadcast_test PRIVATE
GTest::gtest_main GTest::gtest
core dgb
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
dgb_coin pool sharechain)
gtest_add_tests(dgb_block_broadcast_test "" AUTO)

endif()
124 changes: 124 additions & 0 deletions src/impl/dgb/test/block_broadcast_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// ---------------------------------------------------------------------------
// dgb::coin::broadcast_won_block dual-path dispatcher test (#82 broadcaster-
// gate, dispatcher half).
//
// Offline contract slice mirroring src/impl/bch/test/embedded_block_broadcast_test.cpp
// (@90a35536). Locks the dual-path rule the DGB run-loop depends on when it
// wires tracker().m_on_block_found to this dispatcher: BOTH the embedded P2P
// relay (PRIMARY) and the external digibyted submitblock (FALLBACK, fired
// ALWAYS) are attempted, each leg is guarded, landed_first records the race
// winner, and with NEITHER sink live the call is a safe no-op (any()=false,
// no throw). Uses a fake ICoinNode so the dispatch contract is verified without
// the real NodeRPC transport (deferred). p2pool-merged-v36 surface: NONE.
//
// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist
// or it becomes a #143-style NOT_BUILT sentinel that reds master.
// ---------------------------------------------------------------------------

#include <gtest/gtest.h>

#include <string>
#include <vector>

#include "../coin/block_broadcast.hpp"

namespace {

// Controllable external-RPC seam: lets each case set has_rpc() and the
// submit_block_hex() ack independently, and records the call so the "always
// fired" rule is observable.
class FakeSeam : public core::coin::ICoinNode {
public:
bool rpc_present = true;
bool submit_ack = true;
int submit_calls = 0;
bool last_ignore_failure = false;

core::coin::WorkView get_work_view() override { return {}; }

bool submit_block_hex(const std::string&, bool ignore_failure) override {
++submit_calls;
last_ignore_failure = ignore_failure;
return submit_ack;
}

bool is_embedded() const override { return false; }
bool has_rpc() const override { return rpc_present; }
};

const std::vector<unsigned char> kBytes(120, 0x42);
const std::string kHex = "00112233";

} // namespace

// 1) NEITHER sink live -> safe no-op: any()=false, landed_first="none", no throw,
// and a null seam is never dereferenced.
TEST(DgbBlockBroadcast, NoSinkLiveIsSafeNoOp) {
auto r = dgb::coin::broadcast_won_block(/*p2p_relay=*/{}, /*seam=*/nullptr,
kBytes, kHex);
EXPECT_FALSE(r.p2p_sent);
EXPECT_FALSE(r.rpc_ok);
EXPECT_FALSE(r.any());
EXPECT_STREQ(r.landed_first, "none");
}

// 2) P2P-only: relay sink present, no RPC -> p2p wins, fallback skipped cleanly.
TEST(DgbBlockBroadcast, P2pOnly) {
bool relayed = false;
std::vector<unsigned char> seen;
auto relay = [&](const std::vector<unsigned char>& b) { relayed = true; seen = b; };

FakeSeam seam; seam.rpc_present = false;
auto r = dgb::coin::broadcast_won_block(relay, &seam, kBytes, kHex);

EXPECT_TRUE(relayed);
EXPECT_EQ(seen, kBytes);
EXPECT_TRUE(r.p2p_sent);
EXPECT_FALSE(r.rpc_ok);
EXPECT_TRUE(r.any());
EXPECT_STREQ(r.landed_first, "p2p");
EXPECT_EQ(seam.submit_calls, 0); // no RPC sink -> fallback not attempted
}

// 3) RPC-only: no embedded P2P sink, RPC acks -> fallback wins the race.
TEST(DgbBlockBroadcast, RpcOnly) {
FakeSeam seam; seam.rpc_present = true; seam.submit_ack = true;
auto r = dgb::coin::broadcast_won_block(/*p2p_relay=*/{}, &seam, kBytes, kHex);

EXPECT_FALSE(r.p2p_sent);
EXPECT_TRUE(r.rpc_ok);
EXPECT_TRUE(r.any());
EXPECT_STREQ(r.landed_first, "rpc");
EXPECT_EQ(seam.submit_calls, 1);
EXPECT_TRUE(seam.last_ignore_failure); // duplicate must not mask a P2P win
}

// 4) DUAL-PATH: both sinks live -> p2p wins landed_first, but the RPC fallback
// STILL fires (the always-fire rule), and a duplicate ack there is success.
TEST(DgbBlockBroadcast, DualPathAlwaysFiresFallback) {
bool relayed = false;
auto relay = [&](const std::vector<unsigned char>&) { relayed = true; };

FakeSeam seam; seam.rpc_present = true; seam.submit_ack = true;
auto r = dgb::coin::broadcast_won_block(relay, &seam, kBytes, kHex);

EXPECT_TRUE(relayed);
EXPECT_TRUE(r.p2p_sent);
EXPECT_TRUE(r.rpc_ok); // fallback fired even though P2P won
EXPECT_EQ(seam.submit_calls, 1); // ALWAYS-fired, not P2P-only catch
EXPECT_STREQ(r.landed_first, "p2p"); // primary won the race
}

// 5) Both sinks present but RPC no-acks (rejected): p2p still carried the block,
// rpc_ok=false, any()=true (P2P leg succeeded).
TEST(DgbBlockBroadcast, FallbackNoAckDoesNotMaskP2p) {
auto relay = [&](const std::vector<unsigned char>&) {};
FakeSeam seam; seam.rpc_present = true; seam.submit_ack = false;
auto r = dgb::coin::broadcast_won_block(relay, &seam, kBytes, kHex);

EXPECT_TRUE(r.p2p_sent);
EXPECT_FALSE(r.rpc_ok);
EXPECT_TRUE(r.any());
EXPECT_STREQ(r.landed_first, "p2p");
EXPECT_EQ(seam.submit_calls, 1);
}
94 changes: 94 additions & 0 deletions src/impl/dgb/test/coin_node_seam_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// ---------------------------------------------------------------------------
// dgb::coin::CoinNode seam-contract test (#82 broadcaster-gate, RPC-fallback
// half).
//
// Locks the embedded-primary / external-RPC-fallback contract of the DGB
// CoinNode seam (core::coin::ICoinNode) that the won-block dispatch depends on.
// web_server.cpp submits a solved block through exactly this seam:
// m_coin_node->submit_block_hex(hex, false) (core/web_server.cpp)
// which forwards to NodeRPC::submitblock (the external-digibyted fallback that
// MUST persist alongside the embedded path). This test pins the three
// invariants that path relies on, so a regression here is caught before it can
// silently break block submission:
// 1. no work source at all -> empty WorkView, no throw
// 2. embedded present, no RPC -> WorkView sourced from embedded;
// is_embedded()=true, has_rpc()=false
// 3. submit_block_hex w/ no RPC sink -> false (the !m_rpc guard, NOT a throw
// and NOT a crash dereferencing null)
//
// SCOPE: the no-RPC guard + embedded WorkView slice only. The LIVE submitblock
// RPC call and the m_on_block_found tracker callback are wired by the DGB
// run-loop standup (NodeBridge + web_server; main_dgb.cpp is selftest-only
// today) -- that is the remaining #82 slice and is NOT exercised here (rpc=
// nullptr in every case). p2pool-merged-v36 surface: NONE (pure local seam).
//
// Mirrors src/impl/bch/test/coin_node_seam_test.cpp; written gtest-style to
// match the sibling dgb tests (gtest_add_tests AUTO). MUST appear in BOTH the
// test/CMakeLists.txt registration AND the build.yml --target allowlist or it
// becomes a #143-style NOT_BUILT sentinel that reds master.
// ---------------------------------------------------------------------------

#include <gtest/gtest.h>

#include <string>

#include "../coin/coin_node.hpp"

namespace {

// Minimal embedded work source: returns a recognisable WorkData so the test can
// prove the seam moved THIS object's agnostic slice into the WorkView.
class FakeEmbedded : public dgb::coin::CoinNodeInterface {
public:
int calls = 0;

dgb::coin::rpc::WorkData getwork() override {
++calls;
dgb::coin::rpc::WorkData wd;
wd.m_data = nlohmann::json{{"marker", "embedded-dgb"}};
wd.m_hashes.push_back(uint256(static_cast<uint64_t>(0xab)));
wd.m_latency = 42;
return wd;
}

nlohmann::json getblockchaininfo() override { return nlohmann::json::object(); }
bool is_synced() const override { return true; }
};

} // namespace

// 1) No work source configured -> empty view, no throw (1:1 btc/bch/ltc ref).
TEST(DgbCoinNodeSeam, NoSourceEmptyView) {
dgb::coin::CoinNode n(nullptr, nullptr);
EXPECT_FALSE(n.is_embedded());
EXPECT_FALSE(n.has_rpc());
core::coin::WorkView v = n.get_work_view();
EXPECT_TRUE(v.m_data.is_null() || v.m_data.empty());
EXPECT_TRUE(v.m_hashes.empty());
EXPECT_EQ(v.m_latency, 0);
}

// 2) Embedded preferred, no RPC fallback wired -> view comes from embedded.
TEST(DgbCoinNodeSeam, EmbeddedSourcedView) {
FakeEmbedded emb;
dgb::coin::CoinNode n(&emb, /*rpc=*/nullptr);
EXPECT_TRUE(n.is_embedded());
EXPECT_FALSE(n.has_rpc());
core::coin::WorkView v = n.get_work_view();
EXPECT_EQ(emb.calls, 1); // embedded WAS the source
ASSERT_TRUE(v.m_data.contains("marker"));
EXPECT_EQ(v.m_data["marker"], "embedded-dgb");
EXPECT_EQ(v.m_hashes.size(), 1u);
EXPECT_EQ(v.m_latency, 42);
}

// 3) No RPC sink -> submit_block_hex returns false (the !m_rpc guard), not a
// throw and not a null-deref crash. This is the won-block fallback contract.
TEST(DgbCoinNodeSeam, SubmitNoRpcReturnsFalse) {
FakeEmbedded emb;
dgb::coin::CoinNode n(&emb, /*rpc=*/nullptr);
EXPECT_FALSE(n.submit_block_hex("00", /*ignore_failure=*/true));

dgb::coin::CoinNode bare(nullptr, nullptr);
EXPECT_FALSE(bare.submit_block_hex("00", /*ignore_failure=*/false));
}