-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathembedded_tx_select_test.cpp
More file actions
291 lines (259 loc) · 11.8 KB
/
Copy pathembedded_tx_select_test.cpp
File metadata and controls
291 lines (259 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
// ---------------------------------------------------------------------------
// dgb_embedded_tx_select_test -- pins make_mempool_tx_source (embedded_tx_select.cpp),
// the production shaper that turns the embedded Mempool's fee-sorted selection
// into the GBT transactions[] form build_work_template passes through.
//
// CONFORMANCE (frstrtr/p2pool-dgb-scrypt): the per-tx entry shape
// {data,txid,hash,fee} and the total_fees fold are exactly what p2pool's GBT
// consumer reads -- `data` is the with-witness submit bytes, `txid` the legacy
// sha256d, `hash` the wtxid for the witness merkle tree, `fee` the per-tx fee
// (null when unknown). The coinbasevalue fold (subsidy(h)+total_fees) is
// asserted in dgb_embedded_coin_node_test against the #207 SSOT; here we pin
// the byte-level shaping the .cpp does over a real Mempool.
//
// Links the full dgb_coin codec like dgb_embedded_coin_node_test (it compiles
// the tx serialization). MUST be in BOTH build.yml --target allowlists (#143
// NOT_BUILT trap).
// ---------------------------------------------------------------------------
#include <cstdint>
#include <string>
#include <vector>
#include <gtest/gtest.h>
#include <impl/dgb/coin/embedded_tx_select.hpp>
#include <impl/dgb/coin/mempool.hpp>
#include <impl/dgb/coin/transaction.hpp>
#include <impl/dgb/coin/template_builder.hpp>
#include <impl/dgb/coin/embedded_coinbase_value.hpp>
#include <core/pack.hpp>
#include <core/hash.hpp>
#include <core/pow.hpp>
#include <btclibs/util/strencodings.h>
using dgb::coin::Mempool;
using dgb::coin::MutableTransaction;
using dgb::coin::TxIn;
using dgb::coin::TxOut;
using dgb::coin::TX_WITH_WITNESS;
using dgb::coin::compute_txid;
using dgb::coin::make_mempool_tx_source;
namespace {
// A minimal, distinct tx tagged by its output value (mirrors the builder used
// across the dgb won-block tests). `index` keeps the prevout distinct so each
// tx has a distinct txid.
MutableTransaction tagged_tx(int64_t value, uint32_t index)
{
MutableTransaction tx;
tx.version = 1;
tx.locktime = 0;
TxIn in;
in.prevout.hash.SetNull();
in.prevout.index = index;
in.sequence = 0xffffffff;
tx.vin.push_back(in);
TxOut out;
out.value = value;
tx.vout.push_back(out);
return tx;
}
// Expected GBT entry shape for a tx, computed independently of the shaper.
nlohmann::json expect_entry(const MutableTransaction& tx, int64_t fee)
{
auto packed = pack(TX_WITH_WITNESS(tx));
nlohmann::json e;
e["data"] = HexStr(packed.get_span());
e["txid"] = compute_txid(tx).GetHex();
e["hash"] = Hash(packed.get_span()).GetHex();
e["fee"] = fee;
return e;
}
} // namespace
// Two fee-known txs: total_fees == sum, entries shaped {data,txid,hash,fee}.
TEST(DgbEmbeddedTxSelect, ShapesKnownFeeTxsAndSumsFees)
{
Mempool pool;
MutableTransaction a = tagged_tx(10, 0);
MutableTransaction b = tagged_tx(20, 1);
ASSERT_TRUE(pool.add_tx(a));
ASSERT_TRUE(pool.add_tx(b));
pool.set_tx_fee(compute_txid(a), 700);
pool.set_tx_fee(compute_txid(b), 300);
auto source = make_mempool_tx_source(pool, /*max_weight=*/4'000'000);
const auto sel = source();
EXPECT_EQ(sel.total_fees, 1000u); // 700 + 300
ASSERT_TRUE(sel.transactions.is_array());
EXPECT_EQ(sel.transactions.size(), 2u);
// Each selected tx must carry the exact independently-computed entry. The
// selection is feerate-sorted, so match by txid rather than position.
for (const auto& want : {expect_entry(a, 700), expect_entry(b, 300)})
{
bool found = false;
for (const auto& got : sel.transactions)
if (got["txid"] == want["txid"]) { EXPECT_EQ(got, want); found = true; }
EXPECT_TRUE(found) << "missing txid " << want["txid"];
}
}
// Empty mempool -> empty transactions[] + zero fees (truthful absence).
TEST(DgbEmbeddedTxSelect, EmptyPoolEmptySelection)
{
Mempool pool;
auto source = make_mempool_tx_source(pool, /*max_weight=*/4'000'000);
const auto sel = source();
EXPECT_EQ(sel.total_fees, 0u);
ASSERT_TRUE(sel.transactions.is_array());
EXPECT_TRUE(sel.transactions.empty());
}
// Unknown-fee txs are excluded from the template (get_sorted_txs_with_fees
// only emits fee-known txs -- including fee=0 would desync coinbasevalue vs the
// p2pool/daemon GBT figure). So a pool with only unknown-fee txs selects none.
TEST(DgbEmbeddedTxSelect, UnknownFeeTxExcluded)
{
Mempool pool;
ASSERT_TRUE(pool.add_tx(tagged_tx(10, 0))); // no set_tx_fee -> fee_known=false
auto source = make_mempool_tx_source(pool, /*max_weight=*/4'000'000);
const auto sel = source();
EXPECT_EQ(sel.total_fees, 0u);
EXPECT_TRUE(sel.transactions.empty());
}
// ---------------------------------------------------------------------------
// CONFORMANCE: selection ORDER + weight-cap skip semantics.
//
// p2pool consumes the daemon GBT transactions[] in the order Core emits it --
// ancestor-feerate descending (CreateNewBlock). The embedded shaper must emit
// the same ordering so the share's tx set and any p2pool-side fee accounting
// line up. get_sorted_txs_with_fees walks m_feerate_index, a
// std::multimap<double,uint256,std::greater<double>> -> highest feerate first.
// These pin that ordering and the weight-cap skip (a `continue`, NOT a `break`:
// a high-feerate tx that overflows the cap is skipped while a lighter,
// lower-feerate tx further down can still be packed).
// ---------------------------------------------------------------------------
namespace {
// A tx with `n` outputs (each tagged distinct via index), to vary weight.
MutableTransaction wide_tx(uint32_t index, int n_outputs)
{
MutableTransaction tx;
tx.version = 1;
tx.locktime = 0;
TxIn in;
in.prevout.hash.SetNull();
in.prevout.index = index;
in.sequence = 0xffffffff;
tx.vin.push_back(in);
for (int i = 0; i < n_outputs; ++i) {
TxOut out;
out.value = 1000 + i;
tx.vout.push_back(out);
}
return tx;
}
uint32_t tx_weight(const MutableTransaction& tx)
{
uint32_t base = 0, wit = 0, weight = 0;
dgb::coin::compute_tx_weight(tx, base, wit, weight);
return weight;
}
} // namespace
// transactions[] are emitted highest-feerate-first (equal weight -> by fee).
TEST(DgbEmbeddedTxSelect, EmitsFeerateDescendingOrder)
{
Mempool pool;
MutableTransaction lo = tagged_tx(10, 0); // fee 300 -> lowest feerate
MutableTransaction mid = tagged_tx(20, 1); // fee 600
MutableTransaction hi = tagged_tx(30, 2); // fee 900 -> highest feerate
// Insert out of feerate order to prove the index, not arrival, drives order.
ASSERT_TRUE(pool.add_tx(mid));
ASSERT_TRUE(pool.add_tx(lo));
ASSERT_TRUE(pool.add_tx(hi));
pool.set_tx_fee(compute_txid(lo), 300);
pool.set_tx_fee(compute_txid(mid), 600);
pool.set_tx_fee(compute_txid(hi), 900);
auto source = make_mempool_tx_source(pool, /*max_weight=*/4'000'000);
const auto sel = source();
ASSERT_EQ(sel.transactions.size(), 3u);
EXPECT_EQ(sel.transactions[0]["txid"], compute_txid(hi).GetHex());
EXPECT_EQ(sel.transactions[1]["txid"], compute_txid(mid).GetHex());
EXPECT_EQ(sel.transactions[2]["txid"], compute_txid(lo).GetHex());
EXPECT_EQ(sel.total_fees, 1800u);
}
// Weight cap skips an over-cap high-feerate tx but still packs a lighter one
// below it (skip-not-break). total_fees reflects only the packed tx.
TEST(DgbEmbeddedTxSelect, WeightCapSkipsHeavyButPacksLighter)
{
Mempool pool;
MutableTransaction heavy = wide_tx(0, /*n_outputs=*/40); // large weight
MutableTransaction light = tagged_tx(50, 1); // single output
ASSERT_TRUE(pool.add_tx(heavy));
ASSERT_TRUE(pool.add_tx(light));
pool.set_tx_fee(compute_txid(heavy), 10'000'000); // huge fee -> top feerate
pool.set_tx_fee(compute_txid(light), 100); // low feerate, low weight
const uint32_t w_heavy = tx_weight(heavy);
const uint32_t w_light = tx_weight(light);
ASSERT_GT(w_heavy, w_light) << "test needs heavy strictly heavier than light";
// Cap exactly fits `light` but not `heavy`: heavy is first (top feerate),
// overflows -> skipped; light follows and fits.
auto source = make_mempool_tx_source(pool, /*max_weight=*/w_light);
const auto sel = source();
ASSERT_EQ(sel.transactions.size(), 1u);
EXPECT_EQ(sel.transactions[0]["txid"], compute_txid(light).GetHex());
EXPECT_EQ(sel.total_fees, 100u);
}
// ---------------------------------------------------------------------------
// END-TO-END ASSEMBLY CONFORMANCE: the full embedded path the production
// EmbeddedCoinNode wires -- Mempool -> make_mempool_tx_source() selection ->
// WorkTemplateInputs.transactions + (subsidy + total_fees) coinbasevalue ->
// build_work_template(). The isolated shaper tests above and the isolated
// build_work_template pass-through test (dgb_template_builder_test) each pin
// HALF; nothing pins that the two seams COMPOSE faithfully -- that the shaper's
// fee-sorted entries survive into tmpl["transactions"] byte-for-byte and IN
// ORDER, and that the SAME selection's total_fees folds into
// tmpl["coinbasevalue"] through the #188/#207 embedded_coinbase_value SSOT.
// This is the seam where a reorder/drop/double-count would silently desync the
// embedded template from the frstrtr/p2pool-dgb-scrypt GBT consumer.
// ---------------------------------------------------------------------------
TEST(DgbEmbeddedTxSelect, EndToEndTemplateAssemblyConformance)
{
Mempool pool;
MutableTransaction lo = tagged_tx(11, 0); // lowest feerate
MutableTransaction mid = tagged_tx(22, 1);
MutableTransaction hi = tagged_tx(33, 2); // highest feerate
ASSERT_TRUE(pool.add_tx(lo));
ASSERT_TRUE(pool.add_tx(mid));
ASSERT_TRUE(pool.add_tx(hi));
pool.set_tx_fee(compute_txid(lo), 250);
pool.set_tx_fee(compute_txid(mid), 500);
pool.set_tx_fee(compute_txid(hi), 900);
// 1) production shaper selects + shapes the fee-sorted transactions[].
auto source = make_mempool_tx_source(pool, /*max_weight=*/4'000'000);
const auto sel = source();
ASSERT_EQ(sel.transactions.size(), 3u);
EXPECT_EQ(sel.total_fees, 1650u); // 250 + 500 + 900
// 2) coinbasevalue folds total_fees through the embedded SSOT (#188) -- the
// SAME definition the external-daemon GBT path resolves against.
const uint32_t height = 1'000'000;
const uint64_t subsidy = 64'200'000'000ull; // fixed subsidy for the KAT
core::SubsidyFunc subsidy_func = [&](uint32_t) -> uint64_t { return subsidy; };
const uint64_t coinbasevalue =
dgb::coin::embedded_coinbase_value(subsidy_func, height, sel.total_fees);
EXPECT_EQ(coinbasevalue, subsidy + 1650ull);
// 3) assemble the work template from that one selection.
dgb::coin::WorkTemplateInputs in;
in.next_height = height;
in.coinbasevalue = coinbasevalue;
in.curtime = 1'700'000'000;
in.transactions = sel.transactions;
const nlohmann::json tmpl = dgb::coin::build_work_template(in);
// transactions[] survive the seam byte-for-byte AND in feerate-desc order.
ASSERT_TRUE(tmpl["transactions"].is_array());
EXPECT_EQ(tmpl["transactions"], sel.transactions);
ASSERT_EQ(tmpl["transactions"].size(), 3u);
EXPECT_EQ(tmpl["transactions"][0]["txid"], compute_txid(hi).GetHex());
EXPECT_EQ(tmpl["transactions"][1]["txid"], compute_txid(mid).GetHex());
EXPECT_EQ(tmpl["transactions"][2]["txid"], compute_txid(lo).GetHex());
// every entry keeps the full p2pool GBT {data,txid,hash,fee} shape.
for (const auto& e : tmpl["transactions"]) {
EXPECT_TRUE(e.contains("data"));
EXPECT_TRUE(e.contains("txid"));
EXPECT_TRUE(e.contains("hash"));
EXPECT_TRUE(e.contains("fee"));
}
// coinbasevalue carries the folded fee total verbatim (no double count).
EXPECT_EQ(tmpl["coinbasevalue"].get<uint64_t>(), subsidy + 1650ull);
}