mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
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:
co-authored by
Konstantin Ullrich
Omar Hatem
parent
2ed93f40b1
commit
ca00370ca4
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user