@@ -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// ---------------------------------------------------------------------------
810810TEST (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