Skip to content

Commit

Permalink
wallet: add config setting "wallet_fullrbf"
Browse files Browse the repository at this point in the history
  • Loading branch information
SomberNight committed Jan 17, 2024
1 parent 248e50e commit df129ad
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 10 deletions.
9 changes: 9 additions & 0 deletions electrum/simple_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,15 @@ def _default_swapserver_url(self) -> str:
_('If you check this box, your unconfirmed transactions will be consolidated into a single transaction.') + '\n' +
_('This will save fees, but might have unwanted effects in terms of privacy')),
)
WALLET_FULLRBF = ConfigVar(
'wallet_fullrbf', default=False, type_=bool,
short_desc=lambda: _('Allow replacing non-RBF transactions'),
long_desc=lambda: (
_("Allow replacing any transaction, not just those that signal BIP-125 replace-by-fee.\n"
"Note that to broadcast replacements for non-RBF transactions, you need to connect\n"
"to an electrum server that allows as such. Further, only a small percentage of miners\n"
"accept such replacements, so it might take a long time for the transaction to get mined.")),
)
WALLET_MERGE_DUPLICATE_OUTPUTS = ConfigVar(
'wallet_merge_duplicate_outputs', default=False, type_=bool,
short_desc=lambda: _('Merge duplicate outputs'),
Expand Down
34 changes: 34 additions & 0 deletions electrum/tests/test_wallet_vertical.py
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,40 @@ async def _bump_fee_p2wpkh_insane_high_target_fee(self, *, config):
tx.version = 2
self.assertEqual('6b03c00f47cb145ffb632c3ce54dece29b9a980949ef5c574321f7fc83fa2238', tx.txid())

async def test_bump_fee_fullrbf(self):
wallet = self.create_standard_wallet_from_seed('gallery elegant struggle ramp mouse crush divide later maze life asthma crop',
config=self.config)

# bootstrap wallet
funding_tx = Transaction('0200000000010134db753b70b109e3b2794029264155ee5848e014523f2f3907ef31851b25192a0000000000fdffffff02a086010000000000160014b518986cf2f8e3832c8b6a123dc7a1c7e446ffba38bb020000000000160014bfbd54bc8f5122583342613ee627553e6b8d858502463043021f533e885dd1f5fdde1c3686d37b34ae6c481cbcb3260aadc4522abd7681a7000220745f5456b42cbe2166437ae2a7652e1a9d7b2646dcb92bf6c30320ad8f86c46d0121021a49a14049ba577a877ed2fce0eb865b448fdd968a55d862be1e36e546cd42db7f432700')
funding_txid = funding_tx.txid()
self.assertEqual('745b125f426f25bf2a88f4986de9a749df14148ca827ebd3bd6c4ec46bb268f8', funding_txid)
wallet.adb.receive_tx_callback(funding_tx, TX_HEIGHT_UNCONFIRMED)

orig_tx = Transaction('02000000000101f868b26bc44e6cbdd3eb27a88c1414df49a7e96d98f4882abf256f425f125b740000000000feffffff02789b00000000000016001407d3bd97fa803b9ef65b55eb1f7e51da7f88310d60ea000000000000160014ec72d69efd49847d2bc5cc71ee99199d4ae30cc80247304402202984488d2f2c3e2bfba9a4f858c3bdeac753e557682e9b0144a236c5e440eed602200fe7d94d195deef8efddfe8543a2ba79c7d18fd3f0f55a633cbe36c71fac4c5c012102b2a282a8ae615f9299319231ae4a4d61d8673ec782e09b254162abf5845376a582432700')
self.assertEqual('bee4b88d69e3707ef1eb7630fbc2d9355126c12d750f5738e260bed52ba7721e', orig_tx.txid())
wallet.adb.receive_tx_callback(orig_tx, TX_HEIGHT_UNCONFIRMED)
self.assertFalse(orig_tx.is_rbf_enabled()) # note: orig_tx does not signal RBF

self.config.WALLET_FULLRBF = False
with self.assertRaises(CannotBumpFee):
tx = wallet.bump_fee(
tx=tx_from_any(orig_tx.serialize()),
new_fee_rate=60,
strategy=BumpFeeStrategy.PRESERVE_PAYMENT,
)

self.config.WALLET_FULLRBF = True
tx = wallet.bump_fee(
tx=tx_from_any(orig_tx.serialize()),
new_fee_rate=60,
strategy=BumpFeeStrategy.PRESERVE_PAYMENT,
)
tx.locktime = 2573187
tx.version = 2
self.assertEqual('d9e5269786e8a5a83ec82cacbf8449402968b46a963ee76ee86a5bcc2ee4a143', tx.txid())
self.assertTrue(tx.is_rbf_enabled())

@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_cpfp_p2pkh(self, mock_save_db):
wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean')
Expand Down
25 changes: 15 additions & 10 deletions electrum/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -922,8 +922,8 @@ def get_tx_info(self, tx: Transaction) -> TxWalletDetails:
size = tx.estimated_size()
fee_per_byte = fee / size
exp_n = self.config.fee_to_depth(fee_per_byte)
can_bump = (is_any_input_ismine or is_swap) and tx.is_rbf_enabled()
can_dscancel = (is_any_input_ismine and tx.is_rbf_enabled()
can_bump = (is_any_input_ismine or is_swap) and self.can_rbf_tx(tx)
can_dscancel = (is_any_input_ismine and self.can_rbf_tx(tx, is_dscancel=True)
and not all([self.is_mine(txout.address) for txout in tx.outputs()]))
try:
self.cpfp(tx, 0)
Expand All @@ -937,7 +937,7 @@ def get_tx_info(self, tx: Transaction) -> TxWalletDetails:
num_blocks_remainining = max(0, num_blocks_remainining)
status = _('Local (future: {})').format(_('in {} blocks').format(num_blocks_remainining))
can_broadcast = self.network is not None
can_bump = (is_any_input_ismine or is_swap) and tx.is_rbf_enabled()
can_bump = (is_any_input_ismine or is_swap) and self.can_rbf_tx(tx)
else:
status = _("Signed")
can_broadcast = self.network is not None
Expand All @@ -956,7 +956,7 @@ def get_tx_info(self, tx: Transaction) -> TxWalletDetails:
amount = None

if is_lightning_funding_tx:
can_bump = False # would change txid
assert not can_bump # would change txid

return TxWalletDetails(
txid=tx_hash,
Expand Down Expand Up @@ -1708,11 +1708,8 @@ def get_unconfirmed_base_tx_for_batching(self, outputs, coins) -> Optional[Trans
# all inputs should be is_mine
if not all([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()]):
continue
# do not mutate LN funding txs, as that would change their txid
if self.is_lightning_funding_tx(txid):
continue
# tx must have opted-in for RBF (even if local, for consistency)
if not tx.is_rbf_enabled():
if not self.can_rbf_tx(tx):
continue
# reject merge if we need to spend outputs from the base tx
remaining_amount = sum(c.value_sats() for c in coins if c.prevout.txid.hex() != tx.txid())
Expand Down Expand Up @@ -2078,7 +2075,7 @@ def bump_fee(
tx = PartialTransaction.from_tx(tx)
assert isinstance(tx, PartialTransaction)
tx.remove_signatures()
if not tx.is_rbf_enabled():
if not self.can_rbf_tx(tx):
raise CannotBumpFee(_('Transaction is final'))
new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision
tx.add_info_from_wallet(self)
Expand Down Expand Up @@ -2302,6 +2299,14 @@ def _is_rbf_allowed_to_touch_tx_output(self, txout: TxOutput) -> bool:
return False
return True

def can_rbf_tx(self, tx: Transaction, *, is_dscancel: bool = False) -> bool:
# do not mutate LN funding txs, as that would change their txid
if not is_dscancel and self.is_lightning_funding_tx(tx.txid()):
return False
if self.config.WALLET_FULLRBF:
return True
return tx.is_rbf_enabled()

def cpfp(self, tx: Transaction, fee: int) -> Optional[PartialTransaction]:
txid = tx.txid()
for i, o in enumerate(tx.outputs()):
Expand Down Expand Up @@ -2343,7 +2348,7 @@ def dscancel(
assert isinstance(tx, PartialTransaction)
tx.remove_signatures()

if not tx.is_rbf_enabled():
if not self.can_rbf_tx(tx, is_dscancel=True):
raise CannotDoubleSpendTx(_('Transaction is final'))
new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision
tx.add_info_from_wallet(self)
Expand Down

0 comments on commit df129ad

Please sign in to comment.