Skip to content

Commit 23dd87e

Browse files
committed
dash(G1): feerate compare is division-free cross-multiply, not pre-divided double
FeeKey keyed the sorted index on a pre-divided double (fee/base_size). That division rounds, so it can collapse a strict dashcore order into a tie -- or split a dashcore tie into a strict order -- diverging the selection order (and thus template byte-serialization) from the p2pool-dash/dashcore oracle. dashcore CompareTxMemPoolEntryByAncestorFee avoids division by cross-multiplying (fee, size) as doubles; FeeKey now carries (fee, base_size) and reproduces that exact comparison. Equal feerate still breaks by txid ascending (GetHash), as the oracle does. Adds an exhaustive-search divergence-vector KAT that pins the cross-multiply against an independent oracle reimplementation and the real std::set<FeeKey> index. Fenced to src/impl/dash/coin/mempool.hpp + test/test_dash_mempool.cpp; no shared/other-coin/consensus reach.
1 parent adc3491 commit 23dd87e

2 files changed

Lines changed: 108 additions & 12 deletions

File tree

src/impl/dash/coin/mempool.hpp

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,28 @@ struct MempoolEntry {
7474
};
7575

7676
/// Deterministic block-template selection key. Sorts highest-feerate
77-
/// first; ties broken by txid ascending. This makes
78-
/// get_sorted_txs_with_fees byte-reproducible across nodes/runs
79-
/// (equal-feerate txs no longer depend on mempool insertion order) and
80-
/// matches dashcore BlockAssembler ordering
81-
/// (CompareTxMemPoolEntryByAncestorFee breaks feerate ties by GetHash()).
77+
/// first; ties broken by txid ascending. Byte-reproducible across
78+
/// nodes/runs AND bit-for-bit conformant with dashcore BlockAssembler
79+
/// ordering. dashcore CompareTxMemPoolEntryByAncestorFee compares two
80+
/// entries by cross-multiplication of (fee, size) as doubles --
81+
/// f1 = a.fee * b.size vs f2 = b.fee * a.size -- explicitly "avoid
82+
/// division by rewriting (a/b > c/d) as (a*d > c*b)". A pre-divided
83+
/// fee/base_size double rounds where the cross-multiply does not, so the
84+
/// two representations can order a tie-pair differently. We therefore
85+
/// carry (fee, base_size) and reproduce the exact double cross-multiply;
86+
/// equal feerate is broken by GetHash()/txid ascending, as the oracle
87+
/// does. (No ancestor packages in this simplified mempool, so
88+
/// GetModFeeAndSize reduces to the entry's own (fee, base_size).)
8289
struct FeeKey {
83-
double feerate;
84-
uint256 txid;
90+
uint64_t fee; // satoshi
91+
uint32_t base_size; // serialized bytes (>0 for every indexed entry)
92+
uint256 txid;
8593
bool operator<(const FeeKey& o) const {
86-
if (feerate != o.feerate) return feerate > o.feerate; // higher feerate first
87-
return txid < o.txid; // txid asc tiebreak (oracle-conformant)
94+
// dashcore CompareTxMemPoolEntryByAncestorFee, division-free form.
95+
const double f1 = static_cast<double>(fee) * o.base_size;
96+
const double f2 = static_cast<double>(o.fee) * base_size;
97+
if (f1 != f2) return f1 > f2; // higher feerate first
98+
return txid < o.txid; // txid asc tiebreak (oracle-conformant)
8899
}
89100
};
90101

@@ -155,7 +166,7 @@ class Mempool {
155166
// recompute_unknown_fees() resolves them after a UTXO update.
156167
// Uses negative feerate as the multimap key so begin() = best.
157168
if (stored.fee_known) {
158-
m_feerate_index.insert(FeeKey{stored.feerate_satvb(), txid});
169+
m_feerate_index.insert(FeeKey{stored.fee, stored.base_size, txid});
159170
}
160171

161172
// Periodic stats — every 100 entries + first 5 + every eviction.
@@ -259,7 +270,7 @@ class Mempool {
259270
for (auto& [txid, entry] : m_pool) {
260271
if (entry.fee_known) continue;
261272
if (compute_fee_locked(entry, utxo)) {
262-
m_feerate_index.insert(FeeKey{entry.feerate_satvb(), txid});
273+
m_feerate_index.insert(FeeKey{entry.fee, entry.base_size, txid});
263274
resolved_fees += entry.fee;
264275
++resolved;
265276
} else {
@@ -431,7 +442,7 @@ class Mempool {
431442

432443
// Drop from feerate index (only present if fee was known).
433444
if (it->second.fee_known) {
434-
m_feerate_index.erase(FeeKey{it->second.feerate_satvb(), txid});
445+
m_feerate_index.erase(FeeKey{it->second.fee, it->second.base_size, txid});
435446
}
436447

437448
// Drop from spent-outputs index.

test/test_dash_mempool.cpp

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@
2727

2828
#include <algorithm>
2929
#include <cstdint>
30+
#include <set>
3031
#include <vector>
3132

3233
using dash::coin::Mempool;
3334
using dash::coin::MutableTransaction;
3435
using dash::coin::dash_txid;
3536
using dash::coin::BlockType;
37+
using dash::coin::FeeKey;
3638
using ::core::coin::UTXOViewCache;
3739
using ::core::coin::Outpoint;
3840
using ::core::coin::Coin;
@@ -316,3 +318,86 @@ TEST(DashMempool, EqualFeerateSelectionIsTxidAscendingAndInsertionOrderIndepende
316318
EXPECT_EQ(forward, sorted)
317319
<< "equal-feerate ties must resolve to txid-ascending, oracle-conformant order";
318320
}
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

Comments
 (0)