Set anti-fee-sniping locktime (exact-tip) on bitcoin sends (#3385)

* Set anti-fee-sniping locktime on bitcoin sends

Current tip (exact-tip) via shared helper, wired into normal sends (path A) and payjoin/PSBT (path B). 0 when unsynced. Converges with the exact-tip cluster (payjoin-cli, ldk-node, Bull Bitcoin) and skips the ~10% backdate.

* Update cw_bitcoin/lib/locktime.dart

Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>

* Update cw_bitcoin/lib/locktime.dart

Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>

* Update cw_bitcoin/lib/locktime.dart [skip ci]

---------

Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Cindy
2026-08-19 12:43:59 +02:00
committed by GitHub
co-authored by Konstantin Ullrich Omar Hatem
parent 2ed93f40b1
commit ca00370ca4
5 changed files with 81 additions and 2 deletions
+12 -1
View File
@@ -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';
@@ -31,6 +32,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';
@@ -447,8 +449,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;
}
+14
View File
@@ -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';
@@ -448,6 +449,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;
@@ -1516,6 +1526,8 @@ abstract class ElectrumWalletBase
});
}
final locktime = await _antiFeeSnipingLocktime();
BasedBitcoinTransacationBuilder txb;
if (network is BitcoinCashNetwork) {
txb = ForkedTransactionBuilder(
@@ -1540,6 +1552,7 @@ abstract class ElectrumWalletBase
inputOrdering: BitcoinOrdering.shuffle,
outputOrdering: BitcoinOrdering.shuffle,
enableRBF: !estimatedTx.spendsUnconfirmedTX,
locktime: locktime,
);
}
@@ -2306,6 +2319,7 @@ abstract class ElectrumWalletBase
inputOrdering: BitcoinOrdering.shuffle,
outputOrdering: BitcoinOrdering.shuffle,
enableRBF: true,
locktime: await _antiFeeSnipingLocktime(),
);
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
+20
View File
@@ -0,0 +1,20 @@
import "dart:typed_data";
/// 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) => (ByteData(4)..setUint32(0, locktime, Endian.little)).buffer.asUint8List();
+3 -1
View File
@@ -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);
+32
View File
@@ -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]);
});
});
}