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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
test_mweb_builder \
test_address_resolution test_compute_share_target \
test_utxo test_dgb_subsidy dgb_share_test \
rpc_request_test softfork_check_test genesis_check_test algo_select_test \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
v37_test \
-j$(nproc)

Expand Down Expand Up @@ -196,7 +196,7 @@ jobs:
test_mweb_builder \
test_address_resolution test_compute_share_target \
test_utxo test_dgb_subsidy dgb_share_test \
rpc_request_test softfork_check_test genesis_check_test algo_select_test \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
v37_test \
-j$(nproc)
Expand Down
112 changes: 112 additions & 0 deletions src/impl/dgb/coin/dgb_arith256.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#pragma once
// ---------------------------------------------------------------------------
// DGB DigiShield retarget arithmetic at TRUE 256-bit width (M3 §7b).
//
// The DigiShield/MultiShield damped multiply in coin/header_chain.hpp pins
// DigiByte Core CalculateNextWorkRequired:
//
// bnNew = avg_target (arith_uint256)
// bnNew *= damped_timespan <-- 256-bit multiply, OVERFLOW TRUNCATES
// bnNew /= nominal_timespan
//
// Earlier §7b slices used an unsigned __int128 proxy: correct ONLY while the
// expanded target fits 64 bits. A real DigiByte Scrypt target is a full
// arith_uint256 (pow_limit ~2^224); near it, avg_target * damped exceeds
// 2^256 and arith_uint256 DROPS the overflow limb. That truncation is
// CONSENSUS behaviour — a wider (overflow-safe) intermediate would compute a
// DIFFERENT next target and diverge from DigiByte Core / p2pool-merged-v36.
//
// This header is the minimal 256-bit unsigned that reproduces that exact
// truncation, isolated + separately guarded like dgb_digishield.hpp. It is
// header-only and depends ONLY on <cstdint>, so it links into the standalone
// GTest guard (no dgb OBJECT lib, no bitcoin arith_uint256 link) — the same
// constraint algo_select / header_chain tests run under. When the embedded
// DigiByte Core port lands, real arith_uint256 drops in with this same
// multiply-then-divide ordering and the boundary vectors carry over.
// ---------------------------------------------------------------------------

#include <cstdint>

namespace dgb::coin {

// 256-bit unsigned, little-endian limbs (limb[0] least significant). Only the
// operations the DigiShield damped multiply needs: scale-by-u64 (truncating at
// 256 bits exactly as arith_uint256::operator*=), divide-by-u64, and compare.
struct u256 {
uint64_t limb[4] = {0, 0, 0, 0};

// Default-zero, plus an IMPLICIT widening from uint64_t so the field-shape
// swap (M3 7b) leaves every existing uint64-range brace-init / literal
// comparison byte-identical: `target = 4096`, `h.target == expected`,
// `pow_limit != 0` all keep compiling and computing the same value, while
// the embedded-daemon port drops full-width digests through this SAME field.
u256() = default;
u256(uint64_t v) { limb[0] = v; }

static u256 from_u64(uint64_t v) { u256 r; r.limb[0] = v; return r; }

bool fits_u64() const { return limb[1] == 0 && limb[2] == 0 && limb[3] == 0; }
bool is_zero() const { return limb[0] == 0 && limb[1] == 0 && limb[2] == 0 && limb[3] == 0; }
uint64_t low64() const { return limb[0]; }

// Multiply by a 64-bit scalar. Carry past the top limb is DISCARDED, i.e.
// the result is truncated to 256 bits — byte-for-byte what arith_uint256
// does. This is the divergence point vs a 128/wider proxy.
u256 mul_u64(uint64_t m) const {
u256 r;
unsigned __int128 carry = 0;
for (int i = 0; i < 4; ++i) {
unsigned __int128 prod = (unsigned __int128)limb[i] * m + carry;
r.limb[i] = (uint64_t)prod;
carry = prod >> 64;
}
// carry beyond limb[3] dropped -> 256-bit truncation (consensus).
return r;
}

// Divide by a 64-bit scalar (d must be non-zero), truncating toward zero.
// Schoolbook long division from the most-significant limb down.
u256 div_u64(uint64_t d) const {
u256 r;
unsigned __int128 rem = 0;
for (int i = 3; i >= 0; --i) {
unsigned __int128 cur = (rem << 64) | limb[i];
r.limb[i] = (uint64_t)(cur / d);
rem = cur % d;
}
return r;
}

// Add another 256-bit value, truncating carry past limb[3] (256-bit wrap,
// same width discipline as mul_u64). Accumulates the retarget window's
// target sum before the divide-by-count average.
u256& operator+=(const u256& o) {
unsigned __int128 carry = 0;
for (int i = 0; i < 4; ++i) {
unsigned __int128 s = (unsigned __int128)limb[i] + o.limb[i] + carry;
limb[i] = (uint64_t)s;
carry = s >> 64;
}
return *this;
}

friend bool operator<(const u256& a, const u256& b) {
for (int i = 3; i >= 0; --i)
if (a.limb[i] != b.limb[i]) return a.limb[i] < b.limb[i];
return false;
}
friend bool operator>(const u256& a, const u256& b) { return b < a; }
friend bool operator==(const u256& a, const u256& b) {
return a.limb[0] == b.limb[0] && a.limb[1] == b.limb[1]
&& a.limb[2] == b.limb[2] && a.limb[3] == b.limb[3];
}
};

// avg * mul / div at full 256-bit width, in DigiByte Core ordering: MULTIPLY
// FIRST (overflow truncates at 256 bits), THEN divide. Reordering to divide
// first would lose low-order precision and is NOT what consensus computes.
inline u256 mul_div_u256(const u256& avg, uint64_t mul, uint64_t div) {
return avg.mul_u64(mul).div_u64(div);
}

} // namespace dgb::coin
73 changes: 73 additions & 0 deletions src/impl/dgb/coin/dgb_digishield.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#pragma once
// ---------------------------------------------------------------------------
// DGB DigiShield Scrypt-only ancestor walk (M3 §7b — SCRYPT-ONLY VALIDATION).
//
// DigiByte retargets per-block per-algo with DigiShield/MultiShield, NOT
// Bitcoin's 2016-block window. For the V36 Scrypt-only lane the difficulty
// window must walk SCRYPT-ALGO ancestors ONLY. On a mixed-algo chain the
// header sequence interleaves Scrypt headers (full PoW validate) with
// non-Scrypt headers accepted by continuity (work-neutral). Folding a
// continuity header into the retarget window corrupts the Scrypt target AND
// re-introduces the work-neutrality break documented as the THIRD INVARIANT
// in coin/header_chain.hpp. This header isolates that walk so the trap lives
// in one consensus-pinned, separately-guarded place — exactly the failure the
// header_chain.hpp DIGISHIELD INSERTION POINT note warns is "easy to get wrong
// on a mixed-algo chain".
//
// This is the ancestor-selection step only; the per-algo DigiShield target
// math (LWMA/MultiShield) layers on top in the following slice and consumes
// the indices this returns.
//
// Header-only: depends ONLY on coin/dgb_block_algo.hpp + std, so it links into
// the standalone CI guard (GTest-only, no dgb OBJECT lib) like algo_select.
// ---------------------------------------------------------------------------

#include <cstddef>
#include <cstdint>
#include <functional>
#include <vector>

#include <impl/dgb/coin/dgb_block_algo.hpp>

namespace dgb::coin {

// THIRD-INVARIANT work-accounting predicate. Only a Scrypt header credits
// cumulative work / best-chain weight; a continuity (known non-Scrypt) header
// extends the header chain but is work-neutral. This is the single predicate
// HeaderChain::validate() consults before adding a header's work, so the
// invariant cannot drift between the validate() path and the retarget walk.
inline bool header_credits_work(int32_t n_version) noexcept
{
return is_scrypt_header(n_version);
}

// Collect the Scrypt-algo ancestors that form a DigiShield retarget window.
//
// version_at(k) -> nVersion of the header k positions back from the search
// start (k = 0 is the nearest candidate ancestor).
// depth -> how many positions are walkable (chain length behind).
// window -> number of Scrypt ancestors the retarget needs.
//
// Returns the k-indices of the first `window` SCRYPT headers, nearest-first,
// skipping every non-Scrypt (continuity) header. Returns fewer than `window`
// only when the available chain is exhausted (early-chain / genesis region).
// Unknown-algo headers never enter a validated chain (REJECT at ingest), so
// the walk treats anything not Scrypt as a skip.
inline std::vector<std::size_t> scrypt_window_ancestors(
const std::function<int32_t(std::size_t)>& version_at,
std::size_t depth,
std::size_t window)
{
std::vector<std::size_t> out;
if (window == 0) return out;
out.reserve(window);
for (std::size_t k = 0; k < depth && out.size() < window; ++k)
{
if (is_scrypt_header(version_at(k)))
out.push_back(k);
// else: continuity header -> skipped, contributes no window sample
}
return out;
}

} // namespace dgb::coin
Loading
Loading