Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wallet: add config setting "wallet_fullrbf" #8821

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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