Skip to content

Commit a5f04ab

Browse files
committed
dgb: M3 §7b MTP monotonicity ingest guard (nTime > median-of-last-11)
Add the daemon-independent, Scrypt-only-walk-safe half of header time validation: a Scrypt header's nTime must be STRICTLY GREATER than the median timestamp of the tip and its (up to) 10 nearest ancestors (DigiByte Core ContextualCheckBlockHeader "time-too-old"). This is the monotonicity floor the demoted nBits re-derivation gate used to imply, recovered without any difficulty recompute -- it reads only timestamps already in the local header chain, so it is safe on a Scrypt-only walk and needs no V4-MultiShield multi-algo window. median_time_past() walks ALL appended headers regardless of algo, mirroring DGB Core's GetMedianTimePast over the block index: continuity (non-Scrypt) headers DO contribute their timestamps to the median even though their PoW is never validated and they are never rejected on the gate (accept-by-continuity is preserved). Genesis (empty chain) returns INT64_MIN -> unconstrained. A rejection never mutates the chain (checked before push_back). +4 tests INTO header_chain_test (no new gtest target -> no build.yml allowlist change, #143 trap avoided): genesis-unconstrained, strict-greater rejection at/below median, continuity timestamps feed the median, continuity header itself bypasses the gate. header_chain_test 25/25 PASS, build EXIT=0. (cherry picked from commit b084acd)
1 parent 6d6850d commit a5f04ab

2 files changed

Lines changed: 127 additions & 0 deletions

File tree

src/impl/dgb/coin/header_chain.hpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@
3939
// following slice — its inputs are pinned here so the multiply cannot reach a
4040
// continuity header.
4141

42+
#include <algorithm>
4243
#include <cstddef>
4344
#include <cstdint>
4445
#include <functional>
46+
#include <limits>
4547
#include <vector>
4648

4749
#include <impl/dgb/coin/dgb_arith256.hpp>
@@ -253,6 +255,20 @@ class HeaderChain {
253255
// gate would only deepen the wrong single-algo retarget model). See
254256
// V37 backlog: full V4 MultiShield recompute.
255257

258+
// MTP monotonicity (DigiByte Core ContextualCheckBlockHeader
259+
// "time-too-old"): a Scrypt header's nTime must be STRICTLY GREATER
260+
// than the median timestamp of the tip and its (up to) 10 nearest
261+
// ancestors. This is the daemon-independent, Scrypt-only-walk-SAFE
262+
// half of header time validation -- it reads ONLY timestamps already
263+
// in the local header chain, with no difficulty re-derivation (the
264+
// demoted V4-MultiShield recompute). It is also the standard
265+
// anti-timewarp monotonicity floor the demoted nBits gate used to
266+
// imply. Genesis (empty chain) is unconstrained: median_time_past()
267+
// returns INT64_MIN, so the first header always passes. A rejection
268+
// never mutates the chain (checked before push_back).
269+
if (h.n_time <= median_time_past())
270+
return IngestResult::REJECTED;
271+
256272
m_chain.push_back(h);
257273
m_cumulative_work += work_from_target(h.target); // credited
258274
return IngestResult::VALIDATED_SCRYPT;
@@ -305,6 +321,31 @@ class HeaderChain {
305321
uint64_t cumulative_work() const { return m_cumulative_work; }
306322
std::size_t size() const { return m_chain.size(); }
307323

324+
// Bitcoin/DigiByte median-time-past span (ContextualCheckBlockHeader uses
325+
// the tip + its 10 nearest ancestors == 11 timestamps).
326+
static constexpr std::size_t MEDIAN_TIME_SPAN = 11;
327+
328+
// MedianTimePast: median nTime over the tip and its (up to) MEDIAN_TIME_SPAN
329+
// nearest ancestors. Walks ALL appended headers regardless of algo --
330+
// DigiByte Core's GetMedianTimePast walks the block index, which interleaves
331+
// every algo, so continuity (non-Scrypt) headers DO contribute their
332+
// timestamps to the median even though their PoW is never validated. Sorts a
333+
// small fixed-size (<= 11) window and returns the upper-middle element
334+
// (sorted[n/2]), matching DGB Core. Returns INT64_MIN for an empty chain so
335+
// the genesis header is unconstrained.
336+
int64_t median_time_past() const
337+
{
338+
if (m_chain.empty())
339+
return std::numeric_limits<int64_t>::min();
340+
const std::size_t n = std::min(m_chain.size(), MEDIAN_TIME_SPAN);
341+
std::vector<int64_t> times;
342+
times.reserve(n);
343+
for (std::size_t i = 0; i < n; ++i)
344+
times.push_back(m_chain[m_chain.size() - 1 - i].n_time);
345+
std::sort(times.begin(), times.end());
346+
return times[times.size() / 2];
347+
}
348+
308349
private:
309350
// Work proxy: inversely proportional to the target. The full-width field
310351
// narrows to low64() for the proxy, keeping cumulative_work byte-identical

src/impl/dgb/test/header_chain_test.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
#include <cstdint>
2424
#include <cstring>
25+
#include <limits>
2526

2627
#include <gtest/gtest.h>
2728

@@ -560,3 +561,88 @@ TEST(HeaderChainValidate, RealScryptDigestFeedsSatisfactionGate)
560561
EXPECT_EQ(hc.validate_and_append(weak), IngestResult::REJECTED);
561562
EXPECT_EQ(hc.size(), 1u); // unchanged
562563
}
564+
565+
// ---------------------------------------------------------------------------
566+
// MTP MONOTONICITY INGEST GUARD (M3 7b -- ContextualCheckBlockHeader
567+
// "time-too-old"). A Scrypt header's nTime must be STRICTLY GREATER than the
568+
// median timestamp of the tip and its (up to) 10 nearest ancestors. This is the
569+
// daemon-independent, Scrypt-only-walk-safe half of header time validation: it
570+
// reads only timestamps already in the local header chain, no difficulty
571+
// re-derivation (the demoted V4-MultiShield recompute). Every appended header of
572+
// EVERY algo contributes to the median exactly as DGB Core's block index does,
573+
// but only the Scrypt-validated path is rejected on it -- continuity headers are
574+
// accept-by-continuity and bypass the gate.
575+
// ---------------------------------------------------------------------------
576+
TEST(HeaderChainMtp, GenesisIsUnconstrained)
577+
{
578+
// Empty chain -> median_time_past() == INT64_MIN -> any nTime accepted.
579+
HeaderChain hc;
580+
EXPECT_EQ(hc.median_time_past(), std::numeric_limits<int64_t>::min());
581+
EXPECT_EQ(hc.validate_and_append({SCRYPT, 1, 100}),
582+
IngestResult::VALIDATED_SCRYPT);
583+
EXPECT_EQ(hc.size(), 1u);
584+
}
585+
586+
TEST(HeaderChainMtp, RejectsTimeNotStrictlyAboveMedian)
587+
{
588+
// Seed 5 monotone Scrypt headers; MTP over the last 5 is sorted[2] == 1200.
589+
HeaderChain hc;
590+
for (int64_t t : {1000, 1100, 1200, 1300, 1400})
591+
ASSERT_EQ(hc.validate_and_append({SCRYPT, t, 100}),
592+
IngestResult::VALIDATED_SCRYPT);
593+
EXPECT_EQ(hc.median_time_past(), 1200);
594+
595+
const std::size_t size_before = hc.size();
596+
const uint64_t work_before = hc.cumulative_work();
597+
598+
// nTime == median: NOT strictly greater -> reject, no mutation.
599+
EXPECT_EQ(hc.validate_and_append({SCRYPT, 1200, 100}),
600+
IngestResult::REJECTED);
601+
// nTime below median -> reject.
602+
EXPECT_EQ(hc.validate_and_append({SCRYPT, 1150, 100}),
603+
IngestResult::REJECTED);
604+
EXPECT_EQ(hc.size(), size_before);
605+
EXPECT_EQ(hc.cumulative_work(), work_before);
606+
607+
// nTime strictly above median -> accepted.
608+
EXPECT_EQ(hc.validate_and_append({SCRYPT, 1500, 100}),
609+
IngestResult::VALIDATED_SCRYPT);
610+
EXPECT_EQ(hc.size(), size_before + 1);
611+
}
612+
613+
TEST(HeaderChainMtp, ContinuityTimestampsContributeToMedian)
614+
{
615+
// A continuity header is NOT MTP-checked (accept-by-continuity), but its
616+
// timestamp DOES enter the median window -- DGB Core's GetMedianTimePast
617+
// walks every block of every algo.
618+
HeaderChain hc;
619+
ASSERT_EQ(hc.validate_and_append({SCRYPT, 1000, 100}),
620+
IngestResult::VALIDATED_SCRYPT);
621+
// Continuity header far in the future: accepted, bypasses MTP.
622+
ASSERT_EQ(hc.validate_and_append({SHA256D, 5000, 100}),
623+
IngestResult::ACCEPTED_CONTINUITY);
624+
625+
// Median over the last 2 headers (1000, 5000) is sorted[1] == 5000. A Scrypt
626+
// header at 4000 is ABOVE the prior Scrypt time yet <= the continuity-lifted
627+
// median, so it is rejected -- proving the continuity timestamp feeds it.
628+
EXPECT_EQ(hc.median_time_past(), 5000);
629+
EXPECT_EQ(hc.validate_and_append({SCRYPT, 4000, 100}),
630+
IngestResult::REJECTED);
631+
// Strictly above the continuity-lifted median -> accepted.
632+
EXPECT_EQ(hc.validate_and_append({SCRYPT, 6000, 100}),
633+
IngestResult::VALIDATED_SCRYPT);
634+
}
635+
636+
TEST(HeaderChainMtp, ContinuityHeaderItselfBypassesMtp)
637+
{
638+
// A continuity header with a time AT or BELOW the median is still accepted:
639+
// the gate is Scrypt-path only.
640+
HeaderChain hc;
641+
ASSERT_EQ(hc.validate_and_append({SCRYPT, 2000, 100}),
642+
IngestResult::VALIDATED_SCRYPT);
643+
EXPECT_EQ(hc.median_time_past(), 2000);
644+
// A continuity header at 1500 (below the median) is NOT rejected.
645+
EXPECT_EQ(hc.validate_and_append({SKEIN, 1500, 100}),
646+
IngestResult::ACCEPTED_CONTINUITY);
647+
EXPECT_EQ(hc.size(), 2u);
648+
}

0 commit comments

Comments
 (0)