|
6 | 6 | #include <key_io.h>
|
7 | 7 | #include <policy/packages.h>
|
8 | 8 | #include <policy/policy.h>
|
| 9 | +#include <policy/ephemeral_policy.h> |
9 | 10 | #include <policy/truc_policy.h>
|
10 | 11 | #include <primitives/transaction.h>
|
11 | 12 | #include <random.h>
|
@@ -89,6 +90,125 @@ static inline CTransactionRef make_tx(const std::vector<COutPoint>& inputs, int3
|
89 | 90 | return MakeTransactionRef(mtx);
|
90 | 91 | }
|
91 | 92 |
|
| 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 | + |
92 | 212 | BOOST_FIXTURE_TEST_CASE(version3_tests, RegTestingSetup)
|
93 | 213 | {
|
94 | 214 | // Test TRUC policy helper functions
|
|
0 commit comments