|
27 | 27 |
|
28 | 28 | #include <algorithm> |
29 | 29 | #include <cstdint> |
| 30 | +#include <set> |
30 | 31 | #include <vector> |
31 | 32 |
|
32 | 33 | using dash::coin::Mempool; |
33 | 34 | using dash::coin::MutableTransaction; |
34 | 35 | using dash::coin::dash_txid; |
35 | 36 | using dash::coin::BlockType; |
| 37 | +using dash::coin::FeeKey; |
36 | 38 | using ::core::coin::UTXOViewCache; |
37 | 39 | using ::core::coin::Outpoint; |
38 | 40 | using ::core::coin::Coin; |
@@ -316,3 +318,86 @@ TEST(DashMempool, EqualFeerateSelectionIsTxidAscendingAndInsertionOrderIndepende |
316 | 318 | EXPECT_EQ(forward, sorted) |
317 | 319 | << "equal-feerate ties must resolve to txid-ascending, oracle-conformant order"; |
318 | 320 | } |
| 321 | + |
| 322 | + |
| 323 | +// --- G1 byte-parity: feerate compare is dashcore division-free cross-multiply |
| 324 | +// |
| 325 | +// dashcore CompareTxMemPoolEntryByAncestorFee compares two entries by |
| 326 | +// cross-multiplication -- f1 = a.fee * b.size vs f2 = b.fee * a.size -- |
| 327 | +// explicitly to "avoid division by rewriting (a/b > c/d) as (a*d > c*b)". |
| 328 | +// c2pool previously keyed the sorted index on a PRE-DIVIDED double |
| 329 | +// (fee / base_size). That division rounds, so it can collapse a strict |
| 330 | +// dashcore order into a tie -- or split a dashcore tie into a strict |
| 331 | +// order -- making the two representations disagree on the selection |
| 332 | +// order of certain (fee, size) pairs. A different selection order is a |
| 333 | +// different template byte-serialization: a latent G1 byte-parity seam |
| 334 | +// against the p2pool-dash / dashcore oracle. FeeKey now carries |
| 335 | +// (fee, base_size) and reproduces the exact double cross-multiply. |
| 336 | +// |
| 337 | +// Divergence vector (found by exhaustive search). The disagreement only |
| 338 | +// manifests at fee magnitudes >~1e14 sat, where the fee/size division |
| 339 | +// loses ULPs the cross-multiply keeps -- for realistic magnitudes the |
| 340 | +// two representations agree, so this fix is exact-oracle-conformance |
| 341 | +// hardening, not a realistic-value bug: |
| 342 | +// A = (fee 182912374030878, size 3535) |
| 343 | +// B = (fee 4415613369921651, size 85337) |
| 344 | +// Pre-divided doubles: A -> 51743245836.174819946 < B -> 51743245836.174827576, |
| 345 | +// i.e. the OLD code ranks B strictly ABOVE A. |
| 346 | +// Cross-multiply: A.fee*B.size == B.fee*A.size exactly -> a genuine |
| 347 | +// feerate TIE, resolved by txid ascending (dashcore GetHash()). |
| 348 | +TEST(DashMempool, FeerateCompareIsDivisionFreeCrossMultiplyNotPreDividedDouble) |
| 349 | +{ |
| 350 | + // Independent dashcore-style reference (division-free cross-multiply). |
| 351 | + auto oracle_less = [](uint64_t fa, uint32_t sa, const uint256& ta, |
| 352 | + uint64_t fb, uint32_t sb, const uint256& tb) { |
| 353 | + const double f1 = static_cast<double>(fa) * sb; |
| 354 | + const double f2 = static_cast<double>(fb) * sa; |
| 355 | + if (f1 != f2) return f1 > f2; // higher feerate first |
| 356 | + return ta < tb; // txid ascending |
| 357 | + }; |
| 358 | + |
| 359 | + // Two distinct txids; assign the SMALLER to A so a correct |
| 360 | + // (cross-multiply => tie => txid-asc) key orders A before B, whereas |
| 361 | + // the old pre-divide code would have put B first ("B higher feerate"). |
| 362 | + const uint256 t0 = mint_hash(9001); |
| 363 | + const uint256 t1 = mint_hash(9002); |
| 364 | + const uint256 ta = std::min(t0, t1); |
| 365 | + const uint256 tb = std::max(t0, t1); |
| 366 | + ASSERT_TRUE(ta < tb); |
| 367 | + |
| 368 | + const uint64_t fa = 182912374030878ULL; |
| 369 | + const uint32_t sa = 3535u; |
| 370 | + const uint64_t fb = 4415613369921651ULL; |
| 371 | + const uint32_t sb = 85337u; |
| 372 | + |
| 373 | + // Precondition on the vector: a genuine cross-multiply tie that the |
| 374 | + // buggy pre-divide would (wrongly) have seen as a strict order. |
| 375 | + EXPECT_EQ(static_cast<double>(fa) * sb, static_cast<double>(fb) * sa) |
| 376 | + << "vector must be a genuine cross-multiply feerate tie"; |
| 377 | + EXPECT_LT(static_cast<double>(fa) / sa, static_cast<double>(fb) / sb) |
| 378 | + << "vector must be a strict (wrong) order under the old pre-divide"; |
| 379 | + |
| 380 | + const FeeKey A{fa, sa, ta}; |
| 381 | + const FeeKey B{fb, sb, tb}; |
| 382 | + |
| 383 | + // FeeKey resolves the cross-multiply tie by txid ascending => A first. |
| 384 | + EXPECT_TRUE(A < B) << "cross-multiply tie must fall to txid-ascending (A<B)"; |
| 385 | + EXPECT_FALSE(B < A); |
| 386 | + |
| 387 | + // FeeKey ordering must agree with the independent oracle on this pair. |
| 388 | + EXPECT_EQ(A < B, oracle_less(fa, sa, ta, fb, sb, tb)); |
| 389 | + EXPECT_EQ(B < A, oracle_less(fb, sb, tb, fa, sa, ta)); |
| 390 | + |
| 391 | + // The real index type (std::set<FeeKey>) must iterate A best-first. |
| 392 | + std::set<FeeKey> idx{B, A}; |
| 393 | + ASSERT_EQ(idx.size(), 2u); |
| 394 | + EXPECT_EQ(idx.begin()->txid, ta) |
| 395 | + << "best-first iteration must yield A (smaller txid) on the tie"; |
| 396 | + |
| 397 | + // Sanity: the feerate arm still dominates. Strictly higher feerate |
| 398 | + // must precede regardless of a smaller-txid competitor. |
| 399 | + const FeeKey hi{20000u, 250u, tb}; // 80 sat/byte |
| 400 | + const FeeKey lo{10000u, 250u, ta}; // 40 sat/byte, but smaller txid |
| 401 | + EXPECT_TRUE(hi < lo) << "higher feerate must precede regardless of txid"; |
| 402 | + EXPECT_FALSE(lo < hi); |
| 403 | +} |
0 commit comments