wallet: randomise locktime of transactions a bit. also check if stale. (#4967)
This commit is contained in:
@@ -426,6 +426,11 @@ class Blockchain(util.PrintError):
|
|||||||
return None
|
return None
|
||||||
return deserialize_header(h, height)
|
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 get_hash(self, height: int) -> str:
|
||||||
def is_height_checkpoint():
|
def is_height_checkpoint():
|
||||||
within_cp_range = height <= constants.net.max_checkpoint()
|
within_cp_range = height <= constants.net.max_checkpoint()
|
||||||
|
|||||||
@@ -1033,7 +1033,6 @@ class TestWalletSending(TestCaseForTestnet):
|
|||||||
|
|
||||||
class NetworkMock:
|
class NetworkMock:
|
||||||
relay_fee = 1000
|
relay_fee = 1000
|
||||||
def get_local_height(self): return 1325785
|
|
||||||
def run_from_another_thread(self, coro):
|
def run_from_another_thread(self, coro):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
return loop.run_until_complete(coro)
|
return loop.run_until_complete(coro)
|
||||||
@@ -1046,7 +1045,7 @@ class TestWalletSending(TestCaseForTestnet):
|
|||||||
privkeys = ['93NQ7CFbwTPyKDJLXe97jczw33fiLijam2SCZL3Uinz1NSbHrTu', ]
|
privkeys = ['93NQ7CFbwTPyKDJLXe97jczw33fiLijam2SCZL3Uinz1NSbHrTu', ]
|
||||||
network = NetworkMock()
|
network = NetworkMock()
|
||||||
dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2'
|
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())
|
tx_copy = Transaction(tx.serialize())
|
||||||
self.assertEqual('010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400',
|
self.assertEqual('010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400',
|
||||||
|
|||||||
@@ -125,7 +125,8 @@ def sweep_preparations(privkeys, network: 'Network', imax=100):
|
|||||||
return inputs, keypairs
|
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)
|
inputs, keypairs = sweep_preparations(privkeys, network, imax)
|
||||||
total = sum(i.get('value') for i in inputs)
|
total = sum(i.get('value') for i in inputs)
|
||||||
if fee is None:
|
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)))
|
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)]
|
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 = Transaction.from_io(inputs, outputs, locktime=locktime)
|
||||||
tx.set_rbf(True)
|
tx.set_rbf(True)
|
||||||
@@ -146,6 +148,26 @@ def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=N
|
|||||||
return tx
|
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
|
class CannotBumpFee(Exception): pass
|
||||||
|
|
||||||
@@ -692,7 +714,7 @@ class Abstract_Wallet(AddressSynchronizer):
|
|||||||
tx = Transaction.from_io(coins, outputs[:])
|
tx = Transaction.from_io(coins, outputs[:])
|
||||||
|
|
||||||
# Timelock tx to current height.
|
# 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)
|
run_hook('make_unsigned_transaction', self, tx)
|
||||||
return tx
|
return tx
|
||||||
|
|
||||||
@@ -794,7 +816,7 @@ class Abstract_Wallet(AddressSynchronizer):
|
|||||||
continue
|
continue
|
||||||
if delta > 0:
|
if delta > 0:
|
||||||
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs'))
|
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)
|
tx_new = Transaction.from_io(inputs, outputs, locktime=locktime)
|
||||||
return tx_new
|
return tx_new
|
||||||
|
|
||||||
@@ -814,7 +836,7 @@ class Abstract_Wallet(AddressSynchronizer):
|
|||||||
inputs = [item]
|
inputs = [item]
|
||||||
out_address = self.get_unused_address() or address
|
out_address = self.get_unused_address() or address
|
||||||
outputs = [TxOutput(TYPE_ADDRESS, out_address, value - fee)]
|
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)
|
return Transaction.from_io(inputs, outputs, locktime=locktime)
|
||||||
|
|
||||||
def add_input_sig_info(self, txin, address):
|
def add_input_sig_info(self, txin, address):
|
||||||
|
|||||||
Reference in New Issue
Block a user