Skip to content

Commit 80627c9

Browse files
committed
If -spkreuse=0, ensure transactions in mempool always have unique scriptPubKeys
Exceptions: - Multiple inputs in the same transaction are allowed to spend against the same scriptPubKey - The same scriptPubKey may be used in the mempool as both first an output, and then spent in a later transaction's input
1 parent 53b9a01 commit 80627c9

File tree

7 files changed

+96
-0
lines changed

7 files changed

+96
-0
lines changed

src/init.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ void SetupServerArgs(ArgsManager& argsman)
655655
OptionsCategory::NODE_RELAY);
656656
argsman.AddArg("-minrelaytxfee=<amt>", strprintf("Fees (in %s/kvB) smaller than this are considered zero fee for relaying, mining and transaction creation (default: %s)",
657657
CURRENCY_UNIT, FormatMoney(DEFAULT_MIN_RELAY_TX_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
658+
argsman.AddArg("-spkreuse", strprintf("Accept transactions reusing addresses or other pubkey scripts (default: %s)", DEFAULT_SPKREUSE), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
658659
argsman.AddArg("-whitelistforcerelay", strprintf("Add 'forcerelay' permission to whitelisted peers with default permissions. This will relay transactions even if the transactions were already in the mempool. (default: %d)", DEFAULT_WHITELISTFORCERELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
659660
argsman.AddArg("-whitelistrelay", strprintf("Add 'relay' permission to whitelisted peers with default permissions. This will accept relayed transactions even when not relaying transactions (default: %d)", DEFAULT_WHITELISTRELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
660661

@@ -1038,6 +1039,16 @@ bool AppInitParameterInteraction(const ArgsManager& args)
10381039

10391040
if (!g_wallet_init_interface.ParameterInteraction()) return false;
10401041

1042+
{
1043+
std::string strSpkReuse = gArgs.GetArg("-spkreuse", DEFAULT_SPKREUSE);
1044+
// Uses string values so future versions can implement other modes
1045+
if (strSpkReuse == "allow" || gArgs.GetBoolArg("-spkreuse", false)) {
1046+
SpkReuseMode = SRM_ALLOW;
1047+
} else {
1048+
SpkReuseMode = SRM_REJECT;
1049+
}
1050+
}
1051+
10411052
// Option to startup with mocktime set (used for regression testing):
10421053
SetMockTime(args.GetIntArg("-mocktime", 0)); // SetMockTime(0) is a no-op
10431054

src/kernel/mempool_entry.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ struct LockPoints {
3535
CBlockIndex* maxInputBlock{nullptr};
3636
};
3737

38+
enum MemPool_SPK_State {
39+
MSS_UNSEEN = 0,
40+
MSS_SPENT = 1,
41+
MSS_CREATED = 2,
42+
MSS_BOTH = 3,
43+
};
44+
45+
typedef std::map<uint160, enum MemPool_SPK_State> SPKStates_t;
46+
3847
struct CompareIteratorByHash {
3948
// SFINAE for T where T is either a pointer type (e.g., a txiter) or a reference_wrapper<T>
4049
// (e.g. a wrapped CTxMemPoolEntry&)
@@ -186,6 +195,8 @@ class CTxMemPoolEntry
186195

187196
mutable size_t idx_randomized; //!< Index in mempool's txns_randomized
188197
mutable Epoch::Marker m_epoch_marker; //!< epoch when last touched, useful for graph algorithms
198+
199+
SPKStates_t mapSPK;
189200
};
190201

191202
using CTxMemPoolEntryRef = CTxMemPoolEntry::CTxMemPoolEntryRef;

src/policy/policy.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ static constexpr unsigned int MAX_STANDARD_SCRIPTSIG_SIZE{1650};
5454
* only increase the dust limit after prior releases were already not creating
5555
* outputs below the new threshold */
5656
static constexpr unsigned int DUST_RELAY_TX_FEE{3000};
57+
static const std::string DEFAULT_SPKREUSE{"allow"};
5758
/** Default for -minrelaytxfee, minimum relay fee for transactions */
5859
static constexpr unsigned int DEFAULT_MIN_RELAY_TX_FEE{1000};
5960
/** Default for -limitancestorcount, max number of in-mempool ancestors */

src/txmempool.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
#include <consensus/consensus.h>
1212
#include <consensus/tx_verify.h>
1313
#include <consensus/validation.h>
14+
#include <crypto/ripemd160.h>
1415
#include <logging.h>
1516
#include <policy/policy.h>
1617
#include <policy/settings.h>
1718
#include <random.h>
1819
#include <tinyformat.h>
20+
#include <script/script.h>
1921
#include <util/check.h>
2022
#include <util/feefrac.h>
2123
#include <util/moneystr.h>
@@ -51,6 +53,13 @@ bool TestLockPointValidity(CChain& active_chain, const LockPoints& lp)
5153
return true;
5254
}
5355

56+
uint160 ScriptHashkey(const CScript& script)
57+
{
58+
uint160 hash;
59+
CRIPEMD160().Write(script.data(), script.size()).Finalize(hash.begin());
60+
return hash;
61+
}
62+
5463
void CTxMemPool::UpdateForDescendants(txiter updateIt, cacheMap& cachedDescendants,
5564
const std::set<uint256>& setExclude, std::set<uint256>& descendants_to_remove)
5665
{
@@ -477,6 +486,10 @@ void CTxMemPool::addUnchecked(const CTxMemPoolEntry &entry, setEntries &setAnces
477486
txns_randomized.emplace_back(newit->GetSharedTx());
478487
newit->idx_randomized = txns_randomized.size() - 1;
479488

489+
for (auto& vSPK : entry.mapSPK) {
490+
mapUsedSPK[vSPK.first] = MemPool_SPK_State(mapUsedSPK[vSPK.first] | vSPK.second);
491+
}
492+
480493
TRACE3(mempool, added,
481494
entry.GetTx().GetHash().data(),
482495
entry.GetTxSize(),
@@ -521,6 +534,14 @@ void CTxMemPool::removeUnchecked(txiter it, MemPoolRemovalReason reason)
521534
} else
522535
txns_randomized.clear();
523536

537+
for (auto& vSPK : it->mapSPK) {
538+
if (vSPK.second == mapUsedSPK.find(vSPK.first)->second) {
539+
mapUsedSPK.erase(vSPK.first);
540+
} else {
541+
mapUsedSPK[vSPK.first] = MemPool_SPK_State(mapUsedSPK[vSPK.first] & ~vSPK.second);
542+
}
543+
}
544+
524545
totalTxSize -= it->GetTxSize();
525546
m_total_fee -= it->GetFee();
526547
cachedInnerUsage -= it->DynamicMemoryUsage();

src/txmempool.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <policy/feerate.h>
1818
#include <policy/packages.h>
1919
#include <primitives/transaction.h>
20+
#include <script/script.h>
2021
#include <sync.h>
2122
#include <util/epochguard.h>
2223
#include <util/hasher.h>
@@ -200,6 +201,8 @@ class CompareTxMemPoolEntryByAncestorFee
200201
}
201202
};
202203

204+
uint160 ScriptHashkey(const CScript& script);
205+
203206
// Multi_index tag names
204207
struct descendant_score {};
205208
struct entry_time {};
@@ -398,6 +401,9 @@ class CTxMemPool
398401
using Limits = kernel::MemPoolLimits;
399402

400403
uint64_t CalculateDescendantMaximum(txiter entry) const EXCLUSIVE_LOCKS_REQUIRED(cs);
404+
405+
SPKStates_t mapUsedSPK;
406+
401407
private:
402408
typedef std::map<txiter, setEntries, CompareIteratorByHash> cacheMap;
403409

src/validation.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ static constexpr int PRUNE_LOCK_BUFFER{10};
111111
GlobalMutex g_best_block_mutex;
112112
std::condition_variable g_best_block_cv;
113113
uint256 g_best_block;
114+
SpkReuseModes SpkReuseMode;
114115

115116
const CBlockIndex* Chainstate::FindForkInGlobalIndex(const CBlockLocator& locator) const
116117
{
@@ -839,6 +840,9 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
839840
return state.Invalid(TxValidationResult::TX_CONFLICT, "txn-same-nonwitness-data-in-mempool");
840841
}
841842

843+
auto spk_reuse_mode = SpkReuseMode;
844+
SPKStates_t mapSPK;
845+
842846
// Check for conflicts with in-memory transactions
843847
for (const CTxIn &txin : tx.vin)
844848
{
@@ -879,6 +883,19 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
879883
}
880884
}
881885

886+
if (spk_reuse_mode != SRM_ALLOW) {
887+
for (const CTxOut& txout : tx.vout) {
888+
uint160 hashSPK = ScriptHashkey(txout.scriptPubKey);
889+
if (m_pool.mapUsedSPK.find(hashSPK) != m_pool.mapUsedSPK.end()) {
890+
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "txn-mempool-spk-reused");
891+
}
892+
if (mapSPK.find(hashSPK) != mapSPK.end()) {
893+
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "txn-spk-reused-twinoutputs");
894+
}
895+
mapSPK[hashSPK] = MemPool_SPK_State(mapSPK[hashSPK] | MSS_CREATED);
896+
}
897+
}
898+
882899
m_view.SetBackend(m_viewmempool);
883900

884901
const CCoinsViewCache& coins_cache = m_active_chainstate.CoinsTip();
@@ -931,6 +948,27 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
931948
return false; // state filled in by CheckTxInputs
932949
}
933950

951+
if (spk_reuse_mode != SRM_ALLOW) {
952+
for (const CTxIn& txin : tx.vin) {
953+
const Coin &coin = m_view.AccessCoin(txin.prevout);
954+
uint160 hashSPK = ScriptHashkey(coin.out.scriptPubKey);
955+
956+
SPKStates_t::iterator mssit = mapSPK.find(hashSPK);
957+
if (mssit != mapSPK.end()) {
958+
if (mssit->second & MSS_CREATED) {
959+
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "txn-spk-reused-change");
960+
}
961+
}
962+
mssit = m_pool.mapUsedSPK.find(hashSPK);
963+
if (mssit != m_pool.mapUsedSPK.end()) {
964+
if (mssit->second & MSS_SPENT) {
965+
return state.Invalid(TxValidationResult::TX_MEMPOOL_POLICY, "txn-mempool-spk-reused-spend");
966+
}
967+
}
968+
mapSPK[hashSPK] = MemPool_SPK_State(mapSPK[hashSPK] | MSS_SPENT);
969+
}
970+
}
971+
934972
if (m_pool.m_opts.require_standard && !AreInputsStandard(tx, m_view, "bad-txns-input-", reason, ignore_rejects)) {
935973
return state.Invalid(TxValidationResult::TX_INPUTS_NOT_STANDARD, reason);
936974
}
@@ -963,6 +1001,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
9631001
entry.reset(new CTxMemPoolEntry(ptx, ws.m_base_fees, nAcceptTime, m_active_chainstate.m_chain.Height(), entry_sequence,
9641002
fSpendsCoinbase, nSigOpsCost, lock_points.value()));
9651003
ws.m_vsize = entry->GetTxSize();
1004+
entry->mapSPK = mapSPK;
9661005

9671006
if (nSigOpsCost > MAX_STANDARD_TX_SIGOPS_COST)
9681007
MaybeRejectDbg(TxValidationResult::TX_NOT_STANDARD, "bad-txns-too-many-sigops",

src/validation.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ extern std::condition_variable g_best_block_cv;
9090
/** Used to notify getblocktemplate RPC of new tips. */
9191
extern uint256 g_best_block;
9292

93+
enum SpkReuseModes {
94+
SRM_ALLOW,
95+
SRM_REJECT,
96+
};
97+
98+
extern SpkReuseModes SpkReuseMode;
99+
93100
/** Documentation for argument 'checklevel'. */
94101
extern const std::vector<std::string> CHECKLEVEL_DOC;
95102

0 commit comments

Comments
 (0)