Skip to content

Commit fa48baf

Browse files
author
MarcoFalke
committed
wallet: Avoid leaking locktime fingerprint when anti-fee-sniping
1 parent 453803a commit fa48baf

File tree

3 files changed

+96
-30
lines changed

3 files changed

+96
-30
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()

0 commit comments

Comments
 (0)