Skip to content

Commit cebe910

Browse files
committed
Merge #15039: wallet: Avoid leaking nLockTime fingerprint when anti-fee-sniping
fa48baf wallet: Avoid leaking locktime fingerprint when anti-fee-sniping (MarcoFalke) 453803a [test] wallet_txn_clone: Correctly clone txin sequence (MarcoFalke) Pull request description: The wallet sets the locktime to the current height of our active chain. This is fine, as long as our node is connected to other nodes. However, when we fall back and get stuck at a particular height (e.g. taking the wallet offline), the same (potentially unique) locktime is used for all transactions. This makes it easier for passive observers to cluster transactions by wallet. For reference, I visualized "locktime-reuse" with the data: * blocks 545k-555k (both inclusive) * locktimes<=60k * excluding coinbase txs ![distribution of height-based tx locktimes used at least twice](https://user-images.githubusercontent.com/6399679/50446163-b8256d80-0913-11e9-9832-40b76052b2b9.png) Tree-SHA512: 2af259dd8f9f863312e2732d80ca8ba6a20c8d6d1c486b10a48479e1c85ccf13b0c38723740ebadde0f28d321cd9c133ad3e5d1e925472eb27681143bda2d0e7
2 parents d7943bd + fa48baf commit cebe910

File tree

4 files changed

+97
-31
lines changed

4 files changed

+97
-31
lines changed

src/wallet/wallet.cpp

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2516,6 +2516,65 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nC
25162516
return true;
25172517
}
25182518

2519+
static bool IsCurrentForAntiFeeSniping(interfaces::Chain::Lock& locked_chain)
2520+
{
2521+
if (IsInitialBlockDownload()) {
2522+
return false;
2523+
}
2524+
constexpr int64_t MAX_ANTI_FEE_SNIPING_TIP_AGE = 8 * 60 * 60; // in seconds
2525+
if (chainActive.Tip()->GetBlockTime() < (GetTime() - MAX_ANTI_FEE_SNIPING_TIP_AGE)) {
2526+
return false;
2527+
}
2528+
return true;
2529+
}
2530+
2531+
/**
2532+
* Return a height-based locktime for new transactions (uses the height of the
2533+
* current chain tip unless we are not synced with the current chain
2534+
*/
2535+
static uint32_t GetLocktimeForNewTransaction(interfaces::Chain::Lock& locked_chain)
2536+
{
2537+
uint32_t locktime;
2538+
// Discourage fee sniping.
2539+
//
2540+
// For a large miner the value of the transactions in the best block and
2541+
// the mempool can exceed the cost of deliberately attempting to mine two
2542+
// blocks to orphan the current best block. By setting nLockTime such that
2543+
// only the next block can include the transaction, we discourage this
2544+
// practice as the height restricted and limited blocksize gives miners
2545+
// considering fee sniping fewer options for pulling off this attack.
2546+
//
2547+
// A simple way to think about this is from the wallet's point of view we
2548+
// always want the blockchain to move forward. By setting nLockTime this
2549+
// way we're basically making the statement that we only want this
2550+
// transaction to appear in the next block; we don't want to potentially
2551+
// encourage reorgs by allowing transactions to appear at lower heights
2552+
// than the next block in forks of the best chain.
2553+
//
2554+
// Of course, the subsidy is high enough, and transaction volume low
2555+
// enough, that fee sniping isn't a problem yet, but by implementing a fix
2556+
// now we ensure code won't be written that makes assumptions about
2557+
// nLockTime that preclude a fix later.
2558+
if (IsCurrentForAntiFeeSniping(locked_chain)) {
2559+
locktime = chainActive.Height();
2560+
2561+
// Secondly occasionally randomly pick a nLockTime even further back, so
2562+
// that transactions that are delayed after signing for whatever reason,
2563+
// e.g. high-latency mix networks and some CoinJoin implementations, have
2564+
// better privacy.
2565+
if (GetRandInt(10) == 0)
2566+
locktime = std::max(0, (int)locktime - GetRandInt(100));
2567+
} else {
2568+
// If our chain is lagging behind, we can't discourage fee sniping nor help
2569+
// the privacy of high-latency transactions. To avoid leaking a potentially
2570+
// unique "nLockTime fingerprint", set nLockTime to a constant.
2571+
locktime = 0;
2572+
}
2573+
assert(locktime <= (unsigned int)chainActive.Height());
2574+
assert(locktime < LOCKTIME_THRESHOLD);
2575+
return locktime;
2576+
}
2577+
25192578
OutputType CWallet::TransactionChangeType(OutputType change_type, const std::vector<CRecipient>& vecSend)
25202579
{
25212580
// If -changetype is specified, always use that change type.
@@ -2570,37 +2629,8 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std
25702629

25712630
CMutableTransaction txNew;
25722631

2573-
// Discourage fee sniping.
2574-
//
2575-
// For a large miner the value of the transactions in the best block and
2576-
// the mempool can exceed the cost of deliberately attempting to mine two
2577-
// blocks to orphan the current best block. By setting nLockTime such that
2578-
// only the next block can include the transaction, we discourage this
2579-
// practice as the height restricted and limited blocksize gives miners
2580-
// considering fee sniping fewer options for pulling off this attack.
2581-
//
2582-
// A simple way to think about this is from the wallet's point of view we
2583-
// always want the blockchain to move forward. By setting nLockTime this
2584-
// way we're basically making the statement that we only want this
2585-
// transaction to appear in the next block; we don't want to potentially
2586-
// encourage reorgs by allowing transactions to appear at lower heights
2587-
// than the next block in forks of the best chain.
2588-
//
2589-
// Of course, the subsidy is high enough, and transaction volume low
2590-
// enough, that fee sniping isn't a problem yet, but by implementing a fix
2591-
// now we ensure code won't be written that makes assumptions about
2592-
// nLockTime that preclude a fix later.
2593-
txNew.nLockTime = chainActive.Height();
2594-
2595-
// Secondly occasionally randomly pick a nLockTime even further back, so
2596-
// that transactions that are delayed after signing for whatever reason,
2597-
// e.g. high-latency mix networks and some CoinJoin implementations, have
2598-
// better privacy.
2599-
if (GetRandInt(10) == 0)
2600-
txNew.nLockTime = std::max(0, (int)txNew.nLockTime - GetRandInt(100));
2632+
txNew.nLockTime = GetLocktimeForNewTransaction(locked_chain);
26012633

2602-
assert(txNew.nLockTime <= (unsigned int)chainActive.Height());
2603-
assert(txNew.nLockTime < LOCKTIME_THRESHOLD);
26042634
FeeCalculation feeCalc;
26052635
CAmount nFeeNeeded;
26062636
int nBytes;

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@
174174
'wallet_fallbackfee.py',
175175
'feature_minchainwork.py',
176176
'rpc_getblockstats.py',
177+
'wallet_create_tx.py',
177178
'p2p_fingerprint.py',
178179
'feature_uacomment.py',
179180
'wallet_coinbase_category.py',

test/functional/wallet_create_tx.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2018 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
from test_framework.test_framework import BitcoinTestFramework
7+
from test_framework.util import (
8+
assert_equal,
9+
)
10+
11+
12+
class CreateTxWalletTest(BitcoinTestFramework):
13+
def set_test_params(self):
14+
self.setup_clean_chain = False
15+
self.num_nodes = 1
16+
17+
def skip_test_if_missing_module(self):
18+
self.skip_if_no_wallet()
19+
20+
def run_test(self):
21+
self.log.info('Check that we have some (old) blocks and that anti-fee-sniping is disabled')
22+
assert_equal(self.nodes[0].getblockchaininfo()['blocks'], 200)
23+
txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1)
24+
tx = self.nodes[0].decoderawtransaction(self.nodes[0].gettransaction(txid)['hex'])
25+
assert_equal(tx['locktime'], 0)
26+
27+
self.log.info('Check that anti-fee-sniping is enabled when we mine a recent block')
28+
self.nodes[0].generate(1)
29+
txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1)
30+
tx = self.nodes[0].decoderawtransaction(self.nodes[0].gettransaction(txid)['hex'])
31+
assert 0 < tx['locktime'] <= 201
32+
33+
34+
if __name__ == '__main__':
35+
CreateTxWalletTest().main()

test/functional/wallet_txn_clone.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def run_test(self):
6565

6666
# Construct a clone of tx1, to be malleated
6767
rawtx1 = self.nodes[0].getrawtransaction(txid1, 1)
68-
clone_inputs = [{"txid": rawtx1["vin"][0]["txid"], "vout": rawtx1["vin"][0]["vout"]}]
68+
clone_inputs = [{"txid": rawtx1["vin"][0]["txid"], "vout": rawtx1["vin"][0]["vout"], "sequence": rawtx1["vin"][0]["sequence"]}]
6969
clone_outputs = {rawtx1["vout"][0]["scriptPubKey"]["addresses"][0]: rawtx1["vout"][0]["value"],
7070
rawtx1["vout"][1]["scriptPubKey"]["addresses"][0]: rawtx1["vout"][1]["value"]}
7171
clone_locktime = rawtx1["locktime"]

0 commit comments

Comments
 (0)