Skip to content

Commit 2badd70

Browse files
committed
Restrict unconfirmed change around fork.
Around the segwit-light fork (some hours before and after), we should avoid spending unconfirmed outputs: If we do that, it is unclear whether or not the fork will be active in the block that confirms those spends, and thus we can't reliably construct a valid chain. With this change, we introduce a (temporary) measure to disallow those spends in the wallet and mempool policy in a window of time around the planned fork activation.
1 parent 4444568 commit 2badd70

File tree

7 files changed

+199
-2
lines changed

7 files changed

+199
-2
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2020 The DIVI developers
3+
# Distributed under the MIT/X11 software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
# Tests the policy changes in wallet and mempool around the
7+
# segwit-light activation time that prevent spending of
8+
# unconfirmed outputs.
9+
10+
from test_framework import BitcoinTestFramework
11+
from authproxy import JSONRPCException
12+
from util import *
13+
14+
ACTIVATION_TIME = 2_100_000_000
15+
16+
# Offset around the activation time where no restrictions are in place.
17+
NO_RESTRICTIONS = 24 * 3_600
18+
19+
# Offset around the activation time where the wallet disallows
20+
# spending unconfirmed change, but the mempool still accepts it.
21+
WALLET_RESTRICTED = 10 * 3_600
22+
23+
# Offset around the activation time where wallet and mempool
24+
# disallow spending unconfirmed outputs.
25+
MEMPOOL_RESTRICTED = 3_600
26+
27+
28+
class AroundSegwitLightTest (BitcoinTestFramework):
29+
30+
def setup_network (self, split=False):
31+
args = ["-debug", "-spendzeroconfchange"]
32+
self.nodes = start_nodes (1, self.options.tmpdir, extra_args=[args])
33+
self.node = self.nodes[0]
34+
self.is_network_split = False
35+
36+
def build_spending_chain (self, key, addr, initialValue):
37+
"""
38+
Spends the initialValue to addr, and then builds a follow-up
39+
transaction that spends that output again back to addr with
40+
a smaller value (for fees). The second transaction is built
41+
using the raw-transactions API and returned as hex, not
42+
submitted to the node already.
43+
"""
44+
45+
txid = self.node.sendtoaddress (addr, initialValue)
46+
data = self.node.getrawtransaction (txid, 1)
47+
outputIndex = None
48+
for i in range (len (data["vout"])):
49+
if data["vout"][i]["value"] == initialValue:
50+
outputIndex = i
51+
break
52+
assert outputIndex is not None
53+
54+
inputs = [{"txid": data[key], "vout": outputIndex}]
55+
outputs = {addr: initialValue - 10}
56+
tx = self.node.createrawtransaction (inputs, outputs)
57+
signed = self.node.signrawtransaction (tx)
58+
assert signed["complete"]
59+
60+
return signed["hex"]
61+
62+
def expect_unrestricted (self):
63+
"""
64+
Checks that spending of unconfirmed change is possible without
65+
restrictions of wallet or mempool.
66+
"""
67+
68+
balance = self.node.getbalance ()
69+
addr = self.node.getnewaddress ()
70+
71+
self.node.sendtoaddress (addr, balance - 10)
72+
self.node.sendtoaddress (addr, balance - 20)
73+
self.node.setgenerate (True, 1)
74+
75+
def expect_wallet_restricted (self, key):
76+
"""
77+
Checks that the wallet forbids spending unconfirmed change,
78+
while the mempool still allows it.
79+
"""
80+
81+
balance = self.node.getbalance ()
82+
addr = self.node.getnewaddress ()
83+
84+
self.node.sendtoaddress (addr, balance - 10)
85+
assert_raises (JSONRPCException, self.node.sendtoaddress, addr, balance - 20)
86+
self.node.setgenerate (True, 1)
87+
88+
balance = self.node.getbalance ()
89+
tx = self.build_spending_chain (key, addr, balance - 10)
90+
self.node.sendrawtransaction (tx)
91+
self.node.setgenerate (True, 1)
92+
93+
def expect_mempool_restricted (self, key):
94+
"""
95+
Checks that the mempool does not allow spending unconfirmed
96+
outputs (even if the transaction is built and submitted directly),
97+
while blocks should still allow it.
98+
"""
99+
100+
balance = self.node.getbalance ()
101+
addr = self.node.getnewaddress ()
102+
103+
tx = self.build_spending_chain (key, addr, balance - 10)
104+
assert_raises (JSONRPCException, self.node.sendrawtransaction, tx)
105+
self.node.generateblock ({"extratx": [tx]})
106+
107+
def run_test (self):
108+
self.node.setgenerate (True, 30)
109+
110+
# Before restrictions come into force, doing a normal
111+
# spend of unconfirmed change through the wallet is fine.
112+
set_node_times (self.nodes, ACTIVATION_TIME - NO_RESTRICTIONS)
113+
self.expect_unrestricted ()
114+
115+
# Next the wallet doesn't allow those spends, but the mempool
116+
# will (if done directly).
117+
set_node_times (self.nodes, ACTIVATION_TIME - WALLET_RESTRICTED)
118+
self.expect_wallet_restricted ("txid")
119+
120+
# Very close to the fork (on both sides), even the mempool won't
121+
# allow spending unconfirmed change. If we include it directly in
122+
# a block, it works.
123+
set_node_times (self.nodes, ACTIVATION_TIME - MEMPOOL_RESTRICTED)
124+
self.expect_mempool_restricted ("txid")
125+
set_node_times (self.nodes, ACTIVATION_TIME + MEMPOOL_RESTRICTED)
126+
self.node.setgenerate (True, 1)
127+
self.expect_mempool_restricted ("baretxid")
128+
129+
# Finally, we should run into mempool-only or no restrictions
130+
# at all if we go further into the future, away from the fork.
131+
set_node_times (self.nodes, ACTIVATION_TIME + WALLET_RESTRICTED)
132+
self.expect_wallet_restricted ("baretxid")
133+
set_node_times (self.nodes, ACTIVATION_TIME + NO_RESTRICTIONS)
134+
self.expect_unrestricted ()
135+
136+
if __name__ == '__main__':
137+
AroundSegwitLightTest ().main ()

divi/qa/rpc-tests/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
'StakingVaultDeactivation.py',
8484
'StakingVaultSpam.py',
8585
'StakingVaultRedundancy.py',
86+
'around_segwit_light.py',
8687
'forknotify.py',
8788
'getchaintips.py',
8889
'httpbasics.py',

divi/src/ForkActivation.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
#include "chain.h"
88
#include "primitives/block.h"
9+
#include "timedata.h"
910

11+
#include <cmath>
1012
#include <unordered_map>
1113
#include <unordered_set>
1214

@@ -59,3 +61,10 @@ bool ActivationState::IsActive(const Fork f) const
5961
assert(mit != ACTIVATION_TIMES.end());
6062
return currentTime >= mit->second;
6163
}
64+
65+
bool ActivationState::CloseToSegwitLight(const int maxSeconds)
66+
{
67+
const int64_t now = GetAdjustedTime();
68+
const int64_t activation = ACTIVATION_TIMES.at(Fork::SegwitLight);
69+
return std::abs(now - activation) <= maxSeconds;
70+
}

divi/src/ForkActivation.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ class ActivationState
6262
*/
6363
bool IsActive(Fork f) const;
6464

65+
/**
66+
* Returns true if the current time is "close" to the activation of
67+
* segwit-light (before or after), with close being within the given
68+
* number of seconds. This is used for a temporary measure to disallow
69+
* (by mempool and wallet policy) spending of unconfirmed change
70+
* around the fork.
71+
*/
72+
static bool CloseToSegwitLight(int maxSeconds);
73+
6574
};
6675

6776
#endif // FORK_ACTIVATION_H

divi/src/main.cpp

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
#include <TransactionOpCounting.h>
6767
#include <OrphanTransactions.h>
6868
#include <MasternodeModule.h>
69+
#include <MemPoolEntry.h>
6970
#include <IndexDatabaseUpdates.h>
7071
#include <BlockTransactionChecker.h>
7172
#include <NodeState.h>
@@ -114,6 +115,13 @@ extern bool fAddressIndex;
114115
extern bool fSpentIndex;
115116
extern CTxMemPool mempool;
116117

118+
/** Number of seconds around the "segwit light" fork for which we disallow
119+
* spending unconfirmed outputs (to avoid messing up with the change
120+
* itself). This should be smaller than the matching constant for the
121+
* wallet (so the wallet does not construct things the mempool won't
122+
* accept in the end). */
123+
constexpr int SEGWIT_LIGHT_FORBID_SPENDING_ZERO_CONF_SECONDS = 14400;
124+
117125
bool IsFinalTx(const CTransaction& tx, const CChain& activeChain, int nBlockHeight = 0 , int64_t nBlockTime = 0);
118126

119127
CCheckpointServices checkpointsVerifier(GetCurrentChainCheckpoints);
@@ -620,7 +628,7 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState& state, const CTransa
620628
// do all inputs exist?
621629
// Note that this does not check for the presence of actual outputs (see the next check for that),
622630
// only helps filling in pfMissingInputs (to determine missing vs spent).
623-
for (const CTxIn txin : tx.vin) {
631+
for (const auto& txin : tx.vin) {
624632
if (!view.HaveCoins(txin.prevout.hash)) {
625633
if (pfMissingInputs)
626634
*pfMissingInputs = true;
@@ -634,6 +642,21 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState& state, const CTransa
634642
return state.Invalid(error("%s : inputs already spent",__func__),
635643
REJECT_DUPLICATE, "bad-txns-inputs-spent");
636644

645+
// If we are close to the segwit-light activation (before or after),
646+
// do not allow spending of zero-confirmation outputs. This would
647+
// mess up with the fork, as it is not clear whether the fork will be
648+
// active or not when they get confirmed (and thus it is not possible
649+
// to reliably construct a chain of transactions).
650+
if (ActivationState::CloseToSegwitLight(SEGWIT_LIGHT_FORBID_SPENDING_ZERO_CONF_SECONDS)) {
651+
for (const auto& txin : tx.vin) {
652+
const auto* coins = view.AccessCoins(txin.prevout.hash);
653+
assert(coins != nullptr && coins->IsAvailable(txin.prevout.n));
654+
if (coins->nHeight == CTxMemPoolEntry::MEMPOOL_HEIGHT)
655+
return state.DoS(0, error("%s : spending zero-confirmation output around segwit light", __func__),
656+
REJECT_NONSTANDARD, "zero-conf-segwit-light");
657+
}
658+
}
659+
637660
// Bring the best block into scope
638661
view.GetBestBlock();
639662

divi/src/wallet.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ extern Settings& settings;
4545

4646
const FeeAndPriorityCalculator& priorityFeeCalculator = FeeAndPriorityCalculator::instance();
4747

48+
/** Number of seconds around the "segwit light" fork for which we force
49+
* not spending unconfirmed change (to avoid messing up with the change
50+
* itself). This should be larger than the matching constant for the
51+
* mempool (so the wallet does not construct things the mempool won't
52+
* accept in the end). */
53+
constexpr int SEGWIT_LIGHT_DISABLE_SPENDING_ZERO_CONF_SECONDS = 43200;
54+
4855
extern CCriticalSection cs_main;
4956
extern CTxMemPool mempool;
5057

@@ -2045,6 +2052,12 @@ bool CWallet::SelectCoinsMinConf(
20452052
return true;
20462053
}
20472054

2055+
bool CWallet::AllowSpendingZeroConfirmationChange() const
2056+
{
2057+
return allowSpendingZeroConfirmationOutputs
2058+
&& !ActivationState::CloseToSegwitLight(SEGWIT_LIGHT_DISABLE_SPENDING_ZERO_CONF_SECONDS);
2059+
}
2060+
20482061
void AppendOutputs(
20492062
const std::vector<std::pair<CScript, CAmount> >& intendedDestinations,
20502063
CMutableTransaction& txNew)
@@ -2813,7 +2826,7 @@ bool CWallet::IsTrusted(const CWalletTx& walletTransaction) const
28132826
return true;
28142827
if (nDepth < 0)
28152828
return false;
2816-
if (!allowSpendingZeroConfirmationOutputs || !DebitsFunds(walletTransaction, isminetype::ISMINE_SPENDABLE)) // using wtx's cached debit
2829+
if (!AllowSpendingZeroConfirmationChange() || !DebitsFunds(walletTransaction, isminetype::ISMINE_SPENDABLE)) // using wtx's cached debit
28172830
return false;
28182831

28192832
// Trusted if all inputs are from us and are in the mempool:

divi/src/wallet.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@ class CWallet :
200200
isminetype IsMine(const CTxIn& txin) const;
201201
bool IsMine(const CTransaction& tx) const;
202202

203+
/** Returns true if the wallet should allow spending of unconfirmed change.
204+
* This is mostly determined by allowSpendingZeroConfirmationOutputs,
205+
* but is forced to off around the segwit-light fork. */
206+
bool AllowSpendingZeroConfirmationChange() const;
207+
203208
protected:
204209
// CWalletDB: load from disk methods
205210
void LoadWalletTransaction(const CWalletTx& wtxIn) override;

0 commit comments

Comments
 (0)