Skip to content

Commit 71a6ab4

Browse files
committed
test: unit test for CheckEphemeralSpends
1 parent 21d28b2 commit 71a6ab4

File tree

3 files changed

+122
-2
lines changed

3 files changed

+122
-2
lines changed

src/test/txvalidation_tests.cpp

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <key_io.h>
77
#include <policy/packages.h>
88
#include <policy/policy.h>
9+
#include <policy/ephemeral_policy.h>
910
#include <policy/truc_policy.h>
1011
#include <primitives/transaction.h>
1112
#include <random.h>
@@ -89,6 +90,125 @@ static inline CTransactionRef make_tx(const std::vector<COutPoint>& inputs, int3
8990
return MakeTransactionRef(mtx);
9091
}
9192

93+
// Same as make_tx but adds 2 normal outputs and 0-value dust to end of vout
94+
static inline CTransactionRef make_ephemeral_tx(const std::vector<COutPoint>& inputs, int32_t version)
95+
{
96+
CMutableTransaction mtx = CMutableTransaction{};
97+
mtx.version = version;
98+
mtx.vin.resize(inputs.size());
99+
mtx.vout.resize(3);
100+
for (size_t i{0}; i < inputs.size(); ++i) {
101+
mtx.vin[i].prevout = inputs[i];
102+
}
103+
for (auto i{0}; i < 3; ++i) {
104+
mtx.vout[i].scriptPubKey = CScript() << OP_TRUE;
105+
mtx.vout[i].nValue = (i == 2) ? 0 : 10000;
106+
}
107+
return MakeTransactionRef(mtx);
108+
}
109+
110+
BOOST_FIXTURE_TEST_CASE(ephemeral_tests, RegTestingSetup)
111+
{
112+
CTxMemPool& pool = *Assert(m_node.mempool);
113+
LOCK2(cs_main, pool.cs);
114+
TestMemPoolEntryHelper entry;
115+
CTxMemPool::setEntries empty_ancestors;
116+
117+
CFeeRate minrelay(1000);
118+
119+
// Basic transaction with dust
120+
auto grandparent_tx_1 = make_ephemeral_tx(random_outpoints(1), /*version=*/2);
121+
const auto dust_txid = grandparent_tx_1->GetHash();
122+
123+
uint32_t dust_index = 2;
124+
125+
// Child transaction spending dust
126+
auto dust_spend = make_tx({COutPoint{dust_txid, dust_index}}, /*version=*/2);
127+
128+
// We first start with nothing "in the mempool", using package checks
129+
130+
// Trivial single transaction with no dust
131+
BOOST_CHECK(!CheckEphemeralSpends({dust_spend}, minrelay, pool).has_value());
132+
133+
// Now with dust, ok because the tx has no dusty parents
134+
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1}, minrelay, pool).has_value());
135+
136+
// Dust checks pass
137+
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, dust_spend}, CFeeRate(0), pool).has_value());
138+
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, dust_spend}, minrelay, pool).has_value());
139+
140+
auto dust_non_spend = make_tx({COutPoint{dust_txid, dust_index - 1}}, /*version=*/2);
141+
142+
// Child spending non-dust only from parent should be disallowed even if dust otherwise spent
143+
BOOST_CHECK(CheckEphemeralSpends({grandparent_tx_1, dust_non_spend, dust_spend}, minrelay, pool).has_value());
144+
BOOST_CHECK(CheckEphemeralSpends({grandparent_tx_1, dust_spend, dust_non_spend}, minrelay, pool).has_value());
145+
BOOST_CHECK(CheckEphemeralSpends({grandparent_tx_1, dust_non_spend}, minrelay, pool).has_value());
146+
147+
auto grandparent_tx_2 = make_ephemeral_tx(random_outpoints(1), /*version=*/2);
148+
const auto dust_txid_2 = grandparent_tx_2->GetHash();
149+
150+
// Spend dust from one but not another is ok, as long as second grandparent has no child
151+
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, dust_spend}, minrelay, pool).has_value());
152+
153+
auto dust_non_spend_both_parents = make_tx({COutPoint{dust_txid, dust_index}, COutPoint{dust_txid_2, dust_index - 1}}, /*version=*/2);
154+
// But if we spend from the parent, it must spend dust
155+
BOOST_CHECK(CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, dust_non_spend_both_parents}, minrelay, pool).has_value());
156+
157+
auto dust_spend_both_parents = make_tx({COutPoint{dust_txid, dust_index}, COutPoint{dust_txid_2, dust_index}}, /*version=*/2);
158+
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, dust_spend_both_parents}, minrelay, pool).has_value());
159+
160+
// Spending other outputs is also correct, as long as the dusty one is spent
161+
const std::vector<COutPoint> all_outpoints{COutPoint(dust_txid, 0), COutPoint(dust_txid, 1), COutPoint(dust_txid, 2),
162+
COutPoint(dust_txid_2, 0), COutPoint(dust_txid_2, 1), COutPoint(dust_txid_2, 2)};
163+
auto dust_spend_all_outpoints = make_tx(all_outpoints, /*version=*/2);
164+
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, dust_spend_all_outpoints}, minrelay, pool).has_value());
165+
166+
// 2 grandparents with dust <- 1 dust-spending parent with dust <- child with no dust
167+
auto parent_with_dust = make_ephemeral_tx({COutPoint{dust_txid, dust_index}, COutPoint{dust_txid_2, dust_index}}, /*version=*/2);
168+
// Ok for parent to have dust
169+
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, parent_with_dust}, minrelay, pool).has_value());
170+
auto child_no_dust = make_tx({COutPoint{parent_with_dust->GetHash(), dust_index}}, /*version=*/2);
171+
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, parent_with_dust, child_no_dust}, minrelay, pool).has_value());
172+
173+
// 2 grandparents with dust <- 1 dust-spending parent with dust <- child with dust
174+
auto child_with_dust = make_ephemeral_tx({COutPoint{parent_with_dust->GetHash(), dust_index}}, /*version=*/2);
175+
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1, grandparent_tx_2, parent_with_dust, child_with_dust}, minrelay, pool).has_value());
176+
177+
// Tests with parents in mempool
178+
179+
// Nothing in mempool, this should pass for any transaction
180+
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_1}, minrelay, pool).has_value());
181+
182+
// Add first grandparent to mempool and fetch entry
183+
pool.addUnchecked(entry.FromTx(grandparent_tx_1));
184+
185+
// Ignores ancestors that aren't direct parents
186+
BOOST_CHECK(!CheckEphemeralSpends({child_no_dust}, minrelay, pool).has_value());
187+
188+
// Valid spend of dust with grandparent in mempool
189+
BOOST_CHECK(!CheckEphemeralSpends({parent_with_dust}, minrelay, pool).has_value());
190+
191+
// Second grandparent in same package
192+
BOOST_CHECK(!CheckEphemeralSpends({parent_with_dust, grandparent_tx_2}, minrelay, pool).has_value());
193+
// Order in package doesn't matter
194+
BOOST_CHECK(!CheckEphemeralSpends({grandparent_tx_2, parent_with_dust}, minrelay, pool).has_value());
195+
196+
// Add second grandparent to mempool
197+
pool.addUnchecked(entry.FromTx(grandparent_tx_2));
198+
199+
// Only spends single dust out of two direct parents
200+
BOOST_CHECK(CheckEphemeralSpends({dust_non_spend_both_parents}, minrelay, pool).has_value());
201+
202+
// Spends both parents' dust
203+
BOOST_CHECK(!CheckEphemeralSpends({parent_with_dust}, minrelay, pool).has_value());
204+
205+
// Now add dusty parent to mempool
206+
pool.addUnchecked(entry.FromTx(parent_with_dust));
207+
208+
// Passes dust checks even with non-parent ancestors
209+
BOOST_CHECK(!CheckEphemeralSpends({child_no_dust}, minrelay, pool).has_value());
210+
}
211+
92212
BOOST_FIXTURE_TEST_CASE(version3_tests, RegTestingSetup)
93213
{
94214
// Test TRUC policy helper functions

src/test/util/txmempool.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns,
141141
return std::nullopt;
142142
}
143143

144-
std::vector<uint32_t> GetDustIndexes(const CTransactionRef tx_ref, CFeeRate dust_relay_rate)
144+
std::vector<uint32_t> GetDustIndexes(const CTransactionRef& tx_ref, CFeeRate dust_relay_rate)
145145
{
146146
std::vector<uint32_t> dust_indexes;
147147
for (size_t i = 0; i < tx_ref->vout.size(); ++i) {

src/test/util/txmempool.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ void CheckMempoolEphemeralInvariants(const CTxMemPool& tx_pool);
5757
/** Return indexes of the transaction's outputs that are considered dust
5858
* at given dust_relay_rate.
5959
*/
60-
std::vector<uint32_t> GetDustIndexes(const CTransactionRef tx_ref, CFeeRate dust_relay_rate);
60+
std::vector<uint32_t> GetDustIndexes(const CTransactionRef& tx_ref, CFeeRate dust_relay_rate);
6161

6262
/** For every transaction in tx_pool, check TRUC invariants:
6363
* - a TRUC tx's ancestor count must be within TRUC_ANCESTOR_LIMIT

0 commit comments

Comments
 (0)