Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion cw_bitcoin/lib/bitcoin_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin/locktime.dart';
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
import 'package:cw_bitcoin/lightning/lightning_wallet.dart';
import 'package:cw_bitcoin/hardware/bitcoin_ledger_service.dart';
Expand All @@ -30,6 +31,7 @@ import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
Expand Down Expand Up @@ -446,8 +448,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
));
}

final locktime = antiFeeSnipingLocktime(
chainTip: await getCurrentChainTip(),
synced: syncStatus is SyncedSyncStatus,
);

return PSBTTransactionBuild(
inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF, cwOutputs: cwOutputs)
inputs: psbtReadyInputs,
outputs: outputs,
enableRBF: enableRBF,
cwOutputs: cwOutputs,
locktime: locktime)
.psbt;
}

Expand Down
14 changes: 14 additions & 0 deletions cw_bitcoin/lib/electrum_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:isolate';

import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/lightning/lightning_wallet.dart';
import 'package:cw_bitcoin/locktime.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
Expand Down Expand Up @@ -455,6 +456,15 @@ abstract class ElectrumWalletBase
return currentChainTip ?? 0;
}

/// Anti-fee-sniping locktime (current tip, exact-tip), LE-encoded for
/// `BitcoinTransactionBuilder`.
Future<List<int>> _antiFeeSnipingLocktime() async {
return locktimeToBytes(antiFeeSnipingLocktime(
chainTip: await getCurrentChainTip(),
synced: syncStatus is SyncedSyncStatus,
));
}

@override
BitcoinWalletKeys get keys {
String? wif;
Expand Down Expand Up @@ -1517,6 +1527,8 @@ abstract class ElectrumWalletBase
});
}

final locktime = await _antiFeeSnipingLocktime();

BasedBitcoinTransacationBuilder txb;
if (network is BitcoinCashNetwork) {
txb = ForkedTransactionBuilder(
Expand All @@ -1538,6 +1550,7 @@ abstract class ElectrumWalletBase
inputOrdering: BitcoinOrdering.shuffle,
outputOrdering: BitcoinOrdering.none,
enableRBF: !estimatedTx.spendsUnconfirmedTX,
locktime: locktime,
);
}

Expand Down Expand Up @@ -2299,6 +2312,7 @@ abstract class ElectrumWalletBase
inputOrdering: BitcoinOrdering.shuffle,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
locktime: await _antiFeeSnipingLocktime(),
);

final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
Expand Down
23 changes: 23 additions & 0 deletions cw_bitcoin/lib/locktime.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// Anti-fee-sniping locktime, set to the current chain tip (exact-tip).
///
/// Matches the exact-tip cluster (payjoin-cli, ldk-node, Bull Bitcoin) and Core's
/// default for externally-constructed transactions, and takes Cake off its unique
/// `nLockTime = 0` fingerprint. It intentionally skips the ~10% backdate
/// Core/Electrum apply for delayed-broadcast privacy, which does not apply here.
/// Returns 0 when not synced or the tip is unknown, to avoid emitting a stale
/// height.
int antiFeeSnipingLocktime({
required int chainTip,
required bool synced,
}) {
if (!synced || chainTip <= 0) return 0;
return chainTip;
}

/// Little-endian 4-byte encoding for `BtcTransaction.locktime`.
List<int> locktimeToBytes(int locktime) => [
locktime & 0xff,
(locktime >> 8) & 0xff,
(locktime >> 16) & 0xff,
(locktime >> 24) & 0xff,
];
4 changes: 3 additions & 1 deletion cw_bitcoin/lib/psbt/transaction_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ class PSBTTransactionBuild {
{required List<PSBTReadyUtxoWithAddress> inputs,
required List<BitcoinBaseOutput> outputs,
required List<OutputInfo> cwOutputs,
bool enableRBF = true}) {
bool enableRBF = true,
int locktime = 0}) {
psbt.setGlobalTxVersion(2);
psbt.setGlobalFallbackLocktime(locktime);
psbt.setGlobalInputCount(inputs.length);
psbt.setGlobalOutputCount(outputs.length);

Expand Down
32 changes: 32 additions & 0 deletions cw_bitcoin/test/locktime_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:cw_bitcoin/locktime.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('antiFeeSnipingLocktime', () {
test('returns 0 when not synced', () {
expect(antiFeeSnipingLocktime(chainTip: 800000, synced: false), 0);
});

test('returns 0 when chainTip <= 0', () {
expect(antiFeeSnipingLocktime(chainTip: 0, synced: true), 0);
});

test('returns the current tip when synced', () {
expect(antiFeeSnipingLocktime(chainTip: 800000, synced: true), 800000);
});
});

group('locktimeToBytes', () {
test('encodes 0 as four zero bytes', () {
expect(locktimeToBytes(0), [0, 0, 0, 0]);
});

test('encodes a block height little-endian', () {
expect(locktimeToBytes(800000), [0x00, 0x35, 0x0C, 0x00]);
});

test('encodes max 32-bit value', () {
expect(locktimeToBytes(0xFFFFFFFF), [0xFF, 0xFF, 0xFF, 0xFF]);
});
});
}
Loading