Skip to content

Commit bd1f0e2

Browse files
authored
nmc(P1e): work-based reorg fork-choice + 6 KATs (46->52) (#197)
connect_locked now selects the tip by most-cumulative-work (with an equal-work tie-break at the tip height) instead of greatest-height, mirroring btc::coin::HeaderChain. A heavier competing branch reorgs the tip even when shorter; an equal-work sibling at the tip height switches (network-consensus tie-break for min-difficulty testnet). Idempotent reject of known hashes makes the switch flip-flop-proof. New NmcP1eForkChoice suite: heavier-short-branch reorg, lighter-sibling no-reorg, equal-work switch, re-receive stability, post-reorg extend, first-header-is-tip. nmc_auxpow_merkle_test 52/52 PASS. Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
1 parent e3332dd commit bd1f0e2

2 files changed

Lines changed: 163 additions & 8 deletions

File tree

src/impl/nmc/coin/header_chain.hpp

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -729,9 +729,10 @@ class HeaderChain {
729729
// P1e storage leg: shared in-memory connect path for plain and merge-mined
730730
// headers. Caller holds m_mutex. Derives the connected height from the
731731
// parent (or 0 for the chain-root seed), runs the activation gate, and on
732-
// ADMIT inserts the IndexEntry and advances the tip (height-proxy fork
733-
// choice; work-based reorg is a later leg). Returns false -- nothing
734-
// persisted -- on already-known / orphan / activation-gate rejection.
732+
// ADMIT inserts the IndexEntry and advances the tip by WORK-BASED fork
733+
// choice (most-cumulative-work wins, with an equal-work tie-break at the
734+
// tip height). Returns false -- nothing persisted -- on already-known /
735+
// orphan / activation-gate rejection.
735736
bool connect_locked(const BlockHeaderType& header,
736737
const std::optional<AuxPow>& auxpow) {
737738
const uint256 bh = block_hash(header);
@@ -778,18 +779,40 @@ class HeaderChain {
778779
e.block_hash = bh;
779780
e.height = static_cast<uint32_t>(height);
780781
// cumulative-work summation: this header's chain_work is the parent's
781-
// running total plus its own block proof (work-based reorg fork-choice
782-
// is still a later sub-leg; tip selection stays height-proxy below).
782+
// running total plus its own block proof.
783783
e.chain_work = parent_work + get_block_proof(header.m_bits);
784784
e.status = has_auxpow ? HEADER_VALID_CHAIN : HEADER_VALID_TREE;
785785
e.auxpow = auxpow;
786786
const uint256 entry_work = e.chain_work;
787787
m_index.emplace(bh, std::move(e));
788788

789-
if (m_tip.IsNull() || height > static_cast<int32_t>(m_tip_height)) {
790-
m_tip = bh; // height-proxy tip (no work reorg yet)
789+
// Work-based fork choice (mirror of btc::coin::HeaderChain): the best
790+
// chain is the one with the most CUMULATIVE work, not the greatest
791+
// height -- so a heavier competing branch reorgs the tip even when it
792+
// is shorter. An equal-work header at the tip height also switches the
793+
// tip: it represents network consensus on a tie, which matters on
794+
// min-difficulty testnet where every block carries identical work.
795+
// Re-receiving a stored header is the idempotent reject above, so the
796+
// switch cannot flip-flop.
797+
const bool first = m_tip.IsNull();
798+
const bool dominated = entry_work > m_best_work;
799+
const bool equal_at_tip = entry_work == m_best_work
800+
&& height == static_cast<int32_t>(m_tip_height)
801+
&& bh != m_tip;
802+
if (first || dominated || equal_at_tip) {
803+
const uint32_t old_height = m_tip_height;
804+
const uint256 old_tip = m_tip;
805+
m_tip = bh;
791806
m_tip_height = static_cast<uint32_t>(height);
792807
m_best_work = entry_work; // cumulative work at the new tip
808+
if (!first && (equal_at_tip
809+
|| height <= static_cast<int32_t>(old_height))) {
810+
LOG_WARNING << "[EMB-NMC] "
811+
<< (equal_at_tip ? "EQUAL-WORK REORG" : "REORG")
812+
<< " at height " << height
813+
<< ": old_tip=" << old_tip.ToString().substr(0, 16)
814+
<< " new_tip=" << bh.ToString().substr(0, 16);
815+
}
793816
}
794817
return true;
795818
}

src/impl/nmc/test/auxpow_merkle_test.cpp

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,7 @@ TEST(NmcP1eStore, PrematureAuxPowIsRejectedByStoreEvenWhenProofValid)
805805
// ---------------------------------------------------------------------------
806806
// P1e cumulative-work summation sub-leg KATs (NmcP1eWork). chain_work is now
807807
// parent.chain_work + get_block_proof(nBits); cumulative_work() tracks the tip.
808-
// Fork-choice stays height-proxy (work-based reorg is a still-later sub-leg).
808+
// (Tip selection itself is covered by the NmcP1eForkChoice suite below.)
809809
// ---------------------------------------------------------------------------
810810
TEST(NmcP1eWork, GetBlockProofMatchesTwoToThe256OverTargetPlusOne)
811811
{
@@ -881,4 +881,136 @@ TEST(NmcP1eWork, RejectedHeaderDoesNotAdvanceCumulativeWork)
881881
EXPECT_EQ(chain_.cumulative_work(), before); // work unchanged on reject
882882
}
883883

884+
// ---------------------------------------------------------------------------
885+
// P1e work-based reorg fork-choice sub-leg KATs (NmcP1eForkChoice). The tip is
886+
// now the header with the most CUMULATIVE work, not the greatest height: a
887+
// heavier competing branch reorgs the tip even when it is shorter, and an
888+
// equal-work header at the tip height switches the tip (network-consensus
889+
// tie-break). Mirrors btc::coin::HeaderChain fork choice.
890+
// ---------------------------------------------------------------------------
891+
namespace {
892+
constexpr uint32_t kEasyBits = 0x1d00ffffu; // BTC genesis-era target
893+
constexpr uint32_t kHardBits = 0x1c0fffffu; // ~16x more work per block
894+
}
895+
896+
TEST(NmcP1eForkChoice, HeavierSingleBlockReorgsOverLongerLighterChain)
897+
{
898+
using nmc::coin::get_block_proof;
899+
HeaderChain chain_(params_activation(19200));
900+
uint256 z; z.SetNull();
901+
// Long, light chain: g <- a1 <- a2 <- a3 (height 3).
902+
BlockHeaderType g = plain_header(z, kEasyBits, 1);
903+
ASSERT_TRUE(chain_.add_header(g));
904+
BlockHeaderType a1 = plain_header(block_hash(g), kEasyBits, 2);
905+
ASSERT_TRUE(chain_.add_header(a1));
906+
BlockHeaderType a2 = plain_header(block_hash(a1), kEasyBits, 3);
907+
ASSERT_TRUE(chain_.add_header(a2));
908+
BlockHeaderType a3 = plain_header(block_hash(a2), kEasyBits, 4);
909+
ASSERT_TRUE(chain_.add_header(a3));
910+
EXPECT_EQ(chain_.height(), 3u);
911+
EXPECT_EQ(chain_.tip()->block_hash, block_hash(a3));
912+
913+
// One much-heavier sibling at height 1 outweighs the whole easy chain.
914+
EXPECT_TRUE(get_block_proof(kHardBits)
915+
> get_block_proof(kEasyBits) + get_block_proof(kEasyBits)
916+
+ get_block_proof(kEasyBits));
917+
BlockHeaderType b1 = plain_header(block_hash(g), kHardBits, 5);
918+
ASSERT_TRUE(chain_.add_header(b1));
919+
EXPECT_EQ(chain_.height(), 1u); // reorg shortened the tip
920+
EXPECT_EQ(chain_.tip()->block_hash, block_hash(b1));
921+
EXPECT_EQ(chain_.cumulative_work(),
922+
get_block_proof(kEasyBits) + get_block_proof(kHardBits));
923+
}
924+
925+
TEST(NmcP1eForkChoice, LighterSiblingIsStoredButDoesNotReorg)
926+
{
927+
HeaderChain chain_(params_activation(19200));
928+
uint256 z; z.SetNull();
929+
BlockHeaderType g = plain_header(z, kEasyBits, 1);
930+
ASSERT_TRUE(chain_.add_header(g));
931+
BlockHeaderType a1 = plain_header(block_hash(g), kHardBits, 2); // heavy tip
932+
ASSERT_TRUE(chain_.add_header(a1));
933+
uint256 work_at_tip = chain_.cumulative_work();
934+
935+
BlockHeaderType b1 = plain_header(block_hash(g), kEasyBits, 3); // lighter sibling
936+
EXPECT_TRUE(chain_.add_header(b1)); // stored...
937+
EXPECT_TRUE(chain_.has_header(block_hash(b1)));
938+
EXPECT_EQ(chain_.size(), 3u);
939+
EXPECT_EQ(chain_.tip()->block_hash, block_hash(a1)); // ...but tip unchanged
940+
EXPECT_EQ(chain_.cumulative_work(), work_at_tip);
941+
}
942+
943+
TEST(NmcP1eForkChoice, EqualWorkSiblingAtTipHeightSwitchesTip)
944+
{
945+
HeaderChain chain_(params_activation(19200));
946+
uint256 z; z.SetNull();
947+
BlockHeaderType g = plain_header(z, kEasyBits, 1);
948+
ASSERT_TRUE(chain_.add_header(g));
949+
BlockHeaderType a1 = plain_header(block_hash(g), kEasyBits, 2);
950+
ASSERT_TRUE(chain_.add_header(a1));
951+
EXPECT_EQ(chain_.tip()->block_hash, block_hash(a1));
952+
uint256 work_at_tip = chain_.cumulative_work();
953+
954+
// Same parent + same difficulty, different nonce => equal work, new hash.
955+
BlockHeaderType b1 = plain_header(block_hash(g), kEasyBits, 99);
956+
EXPECT_TRUE(chain_.add_header(b1));
957+
EXPECT_EQ(chain_.tip()->block_hash, block_hash(b1)); // equal-work tie-break switch
958+
EXPECT_EQ(chain_.height(), 1u);
959+
EXPECT_EQ(chain_.cumulative_work(), work_at_tip); // work identical
960+
}
961+
962+
TEST(NmcP1eForkChoice, ReReceivingHeadersDoesNotFlipFlopTheTip)
963+
{
964+
HeaderChain chain_(params_activation(19200));
965+
uint256 z; z.SetNull();
966+
BlockHeaderType g = plain_header(z, kEasyBits, 1);
967+
ASSERT_TRUE(chain_.add_header(g));
968+
BlockHeaderType a1 = plain_header(block_hash(g), kEasyBits, 2);
969+
ASSERT_TRUE(chain_.add_header(a1));
970+
BlockHeaderType b1 = plain_header(block_hash(g), kHardBits, 3); // reorg to heavy
971+
ASSERT_TRUE(chain_.add_header(b1));
972+
EXPECT_EQ(chain_.tip()->block_hash, block_hash(b1));
973+
uint256 work_at_tip = chain_.cumulative_work();
974+
975+
// Re-receiving either stored header is an idempotent no-op; tip is stable.
976+
EXPECT_FALSE(chain_.add_header(a1));
977+
EXPECT_FALSE(chain_.add_header(b1));
978+
EXPECT_EQ(chain_.tip()->block_hash, block_hash(b1));
979+
EXPECT_EQ(chain_.cumulative_work(), work_at_tip);
980+
}
981+
982+
TEST(NmcP1eForkChoice, TipExtendsAfterAWorkReorg)
983+
{
984+
using nmc::coin::get_block_proof;
985+
HeaderChain chain_(params_activation(19200));
986+
uint256 z; z.SetNull();
987+
BlockHeaderType g = plain_header(z, kEasyBits, 1);
988+
ASSERT_TRUE(chain_.add_header(g));
989+
BlockHeaderType a1 = plain_header(block_hash(g), kEasyBits, 2);
990+
ASSERT_TRUE(chain_.add_header(a1));
991+
BlockHeaderType b1 = plain_header(block_hash(g), kHardBits, 3); // reorg
992+
ASSERT_TRUE(chain_.add_header(b1));
993+
EXPECT_EQ(chain_.height(), 1u);
994+
995+
BlockHeaderType b2 = plain_header(block_hash(b1), kEasyBits, 4); // extend heavy tip
996+
ASSERT_TRUE(chain_.add_header(b2));
997+
EXPECT_EQ(chain_.height(), 2u);
998+
EXPECT_EQ(chain_.tip()->block_hash, block_hash(b2));
999+
EXPECT_EQ(chain_.cumulative_work(),
1000+
get_block_proof(kEasyBits) + get_block_proof(kHardBits)
1001+
+ get_block_proof(kEasyBits));
1002+
}
1003+
1004+
TEST(NmcP1eForkChoice, FirstHeaderBecomesTipRegardlessOfPriorState)
1005+
{
1006+
HeaderChain chain_(params_activation(19200));
1007+
EXPECT_FALSE(chain_.tip().has_value());
1008+
uint256 z; z.SetNull();
1009+
BlockHeaderType g = plain_header(z, kEasyBits, 1);
1010+
ASSERT_TRUE(chain_.add_header(g));
1011+
ASSERT_TRUE(chain_.tip().has_value());
1012+
EXPECT_EQ(chain_.tip()->block_hash, block_hash(g));
1013+
EXPECT_EQ(chain_.cumulative_work(), nmc::coin::get_block_proof(kEasyBits));
1014+
}
1015+
8841016
} // namespace

0 commit comments

Comments
 (0)