Skip to content

Commit 707e9b6

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 ab7be35 commit 707e9b6

File tree

7 files changed

+200
-2
lines changed

7 files changed

+200
-2
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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.expect_mempool_restricted ("txid")
127+
128+
# Finally, we should run into mempool-only or no restrictions
129+
# at all if we go further into the future, away from the fork.
130+
set_node_times (self.nodes, ACTIVATION_TIME + WALLET_RESTRICTED)
131+
self.expect_wallet_restricted ("txid")
132+
set_node_times (self.nodes, ACTIVATION_TIME + NO_RESTRICTIONS)
133+
self.expect_unrestricted ()
134+
135+
if __name__ == '__main__':
136+
AroundSegwitLightTest ().main ()

divi/qa/rpc-tests/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
'StakingVaultStaking.py',
8181
'StakingVaultDeactivation.py',
8282
'StakingVaultSpam.py',
83+
'around_segwit_light.py',
8384
'forknotify.py',
8485
'getchaintips.py',
8586
'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

1214
#include <Settings.h>
@@ -54,3 +56,10 @@ bool ActivationState::IsActive(const Fork f) const
5456
assert(mit != ACTIVATION_TIMES.end());
5557
return nTime >= mit->second;
5658
}
59+
60+
bool ActivationState::CloseToSegwitLight(const int maxSeconds)
61+
{
62+
const int64_t now = GetAdjustedTime();
63+
const int64_t activation = ACTIVATION_TIMES.at(Fork::SegwitLight);
64+
return std::abs(now - activation) <= maxSeconds;
65+
}

divi/src/ForkActivation.h

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

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

6877
#endif // FORK_ACTIVATION_H

divi/src/main.cpp

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ bool fSpentIndex = false;
113113
bool fCheckBlockIndex = false;
114114
bool fVerifyingBlocks = false;
115115
unsigned int nCoinCacheSize = 5000;
116+
117+
/** Number of seconds around the "segwit light" fork for which we disallow
118+
* spending unconfirmed outputs (to avoid messing up with the change
119+
* itself). This should be smaller than the matching constant for the
120+
* wallet (so the wallet does not construct things the mempool won't
121+
* accept in the end). */
122+
constexpr int SEGWIT_LIGHT_FORBID_SPENDING_ZERO_CONF_SECONDS = 14400;
123+
116124
bool IsFinalTx(const CTransaction& tx, const CChain& activeChain, int nBlockHeight = 0 , int64_t nBlockTime = 0);
117125

118126
CCheckpointServices checkpointsVerifier(GetCurrentChainCheckpoints);
@@ -594,7 +602,7 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState& state, const CTransa
594602
// do all inputs exist?
595603
// Note that this does not check for the presence of actual outputs (see the next check for that),
596604
// only helps filling in pfMissingInputs (to determine missing vs spent).
597-
for (const CTxIn txin : tx.vin) {
605+
for (const auto& txin : tx.vin) {
598606
if (!view.HaveCoins(txin.prevout.hash)) {
599607
if (pfMissingInputs)
600608
*pfMissingInputs = true;
@@ -607,6 +615,21 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState& state, const CTransa
607615
return state.Invalid(error("%s : inputs already spent",__func__),
608616
REJECT_DUPLICATE, "bad-txns-inputs-spent");
609617

618+
// If we are close to the segwit-light activation (before or after),
619+
// do not allow spending of zero-confirmation outputs. This would
620+
// mess up with the fork, as it is not clear whether the fork will be
621+
// active or not when they get confirmed (and thus it is not possible
622+
// to reliably construct a chain of transactions).
623+
if (ActivationState::CloseToSegwitLight(SEGWIT_LIGHT_FORBID_SPENDING_ZERO_CONF_SECONDS)) {
624+
for (const auto& txin : tx.vin) {
625+
const auto* coins = view.AccessCoins(txin.prevout.hash);
626+
assert(coins != nullptr && coins->IsAvailable(txin.prevout.n));
627+
if (coins->nHeight == MEMPOOL_HEIGHT)
628+
return state.DoS(0, error("%s : spending zero-confirmation output around segwit light", __func__),
629+
REJECT_NONSTANDARD, "zero-conf-segwit-light");
630+
}
631+
}
632+
610633
// Bring the best block into scope
611634
view.GetBestBlock();
612635

divi/src/wallet.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <chain.h>
1515
#include "coincontrol.h"
1616
#include <chainparams.h>
17+
#include "ForkActivation.h"
1718
#include "masternode-payments.h"
1819
#include "net.h"
1920
#include "script/script.h"
@@ -58,6 +59,14 @@ using namespace std;
5859
CAmount nTransactionValueMultiplier = 10000; // 1 / 0.0001 = 10000;
5960
unsigned int nTransactionSizeMultiplier = 300;
6061
static const unsigned int DEFAULT_KEYPOOL_SIZE = 1000;
62+
63+
/** Number of seconds around the "segwit light" fork for which we force
64+
* not spending unconfirmed change (to avoid messing up with the change
65+
* itself). This should be larger than the matching constant for the
66+
* mempool (so the wallet does not construct things the mempool won't
67+
* accept in the end). */
68+
constexpr int SEGWIT_LIGHT_DISABLE_SPENDING_ZERO_CONF_SECONDS = 43200;
69+
6170
int64_t nStartupTime = GetAdjustedTime();
6271

6372
/** @defgroup mapWallet
@@ -2023,6 +2032,12 @@ void CWallet::UpdateNextTransactionIndexAvailable(int64_t transactionIndex)
20232032
orderedTransactionIndex = transactionIndex;
20242033
}
20252034

2035+
bool CWallet::AllowSpendingZeroConfirmationChange() const
2036+
{
2037+
return allowSpendingZeroConfirmationOutputs
2038+
&& !ActivationState::CloseToSegwitLight(SEGWIT_LIGHT_DISABLE_SPENDING_ZERO_CONF_SECONDS);
2039+
}
2040+
20262041
void AppendOutputs(
20272042
const std::vector<std::pair<CScript, CAmount> >& intendedDestinations,
20282043
CMutableTransaction& txNew)
@@ -2834,7 +2849,7 @@ bool CWallet::IsTrusted(const CWalletTx& walletTransaction) const
28342849
return true;
28352850
if (nDepth < 0)
28362851
return false;
2837-
if (!allowSpendingZeroConfirmationOutputs || !DebitsFunds(walletTransaction, ISMINE_ALL)) // using wtx's cached debit
2852+
if (!AllowSpendingZeroConfirmationChange() || !DebitsFunds(walletTransaction, ISMINE_ALL)) // using wtx's cached debit
28382853
return false;
28392854

28402855
// 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
@@ -166,6 +166,11 @@ class CWallet : public CCryptoKeyStore, public NotificationInterface, public I_K
166166
private:
167167
void DeriveNewChildKey(const CKeyMetadata& metadata, CKey& secretRet, uint32_t nAccountIndex, bool fInternal /*= false*/);
168168

169+
/** Returns true if the wallet should allow spending of unconfirmed change.
170+
* This is mostly determined by allowSpendingZeroConfirmationOutputs,
171+
* but is forced to off around the segwit-light fork. */
172+
bool AllowSpendingZeroConfirmationChange() const;
173+
169174
public:
170175
bool MoveFundsBetweenAccounts(std::string from, std::string to, CAmount amount, std::string comment);
171176

0 commit comments

Comments
 (0)