commit dc19cf1fa1083b4236755621a36c3642bf7a3bfc
parent d5c8a0e0d0597bc510b79b76c73c26689f3d692d
Author: ghost43 <somber.night@protonmail.com>
Date: Wed, 16 Jan 2019 18:51:59 +0100
wallet: randomise locktime of transactions a bit. also check if stale. (#4967)
Diffstat:
3 files changed, 33 insertions(+), 7 deletions(-)
diff --git a/electrum/blockchain.py b/electrum/blockchain.py
@@ -426,6 +426,11 @@ class Blockchain(util.PrintError):
return None
return deserialize_header(h, height)
+ def header_at_tip(self) -> Optional[dict]:
+ """Return latest header."""
+ height = self.height()
+ return self.read_header(height)
+
def get_hash(self, height: int) -> str:
def is_height_checkpoint():
within_cp_range = height <= constants.net.max_checkpoint()
diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py
@@ -1033,7 +1033,6 @@ class TestWalletSending(TestCaseForTestnet):
class NetworkMock:
relay_fee = 1000
- def get_local_height(self): return 1325785
def run_from_another_thread(self, coro):
loop = asyncio.get_event_loop()
return loop.run_until_complete(coro)
@@ -1046,7 +1045,7 @@ class TestWalletSending(TestCaseForTestnet):
privkeys = ['93NQ7CFbwTPyKDJLXe97jczw33fiLijam2SCZL3Uinz1NSbHrTu', ]
network = NetworkMock()
dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2'
- tx = sweep(privkeys, network, config=None, recipient=dest_addr, fee=5000)
+ tx = sweep(privkeys, network, config=None, recipient=dest_addr, fee=5000, locktime=1325785)
tx_copy = Transaction(tx.serialize())
self.assertEqual('010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400',
diff --git a/electrum/wallet.py b/electrum/wallet.py
@@ -125,7 +125,8 @@ def sweep_preparations(privkeys, network: 'Network', imax=100):
return inputs, keypairs
-def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=None, imax=100):
+def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=None, imax=100,
+ *, locktime=None):
inputs, keypairs = sweep_preparations(privkeys, network, imax)
total = sum(i.get('value') for i in inputs)
if fee is None:
@@ -138,7 +139,8 @@ def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=N
raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network)))
outputs = [TxOutput(TYPE_ADDRESS, recipient, total - fee)]
- locktime = network.get_local_height()
+ if locktime is None:
+ locktime = get_locktime_for_new_transaction(network)
tx = Transaction.from_io(inputs, outputs, locktime=locktime)
tx.set_rbf(True)
@@ -146,6 +148,26 @@ def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=N
return tx
+def get_locktime_for_new_transaction(network: 'Network') -> int:
+ # if no network or not up to date, just set locktime to zero
+ if not network:
+ return 0
+ chain = network.blockchain()
+ header = chain.header_at_tip()
+ if not header:
+ return 0
+ STALE_DELAY = 8 * 60 * 60 # in seconds
+ if header['timestamp'] + STALE_DELAY < time.time():
+ return 0
+ # discourage "fee sniping"
+ locktime = chain.height()
+ # sometimes pick locktime a bit further back, to help privacy
+ # of setups that need more time (offline/multisig/coinjoin/...)
+ if random.randint(0, 9) == 0:
+ locktime = max(0, locktime - random.randint(0, 99))
+ return locktime
+
+
class CannotBumpFee(Exception): pass
@@ -692,7 +714,7 @@ class Abstract_Wallet(AddressSynchronizer):
tx = Transaction.from_io(coins, outputs[:])
# Timelock tx to current height.
- tx.locktime = self.get_local_height()
+ tx.locktime = get_locktime_for_new_transaction(self.network)
run_hook('make_unsigned_transaction', self, tx)
return tx
@@ -794,7 +816,7 @@ class Abstract_Wallet(AddressSynchronizer):
continue
if delta > 0:
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs'))
- locktime = self.get_local_height()
+ locktime = get_locktime_for_new_transaction(self.network)
tx_new = Transaction.from_io(inputs, outputs, locktime=locktime)
return tx_new
@@ -814,7 +836,7 @@ class Abstract_Wallet(AddressSynchronizer):
inputs = [item]
out_address = self.get_unused_address() or address
outputs = [TxOutput(TYPE_ADDRESS, out_address, value - fee)]
- locktime = self.get_local_height()
+ locktime = get_locktime_for_new_transaction(self.network)
return Transaction.from_io(inputs, outputs, locktime=locktime)
def add_input_sig_info(self, txin, address):