Skip to content

Commit

Permalink
Add CoinSelection for specified inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewLM committed Nov 14, 2023
1 parent f43b890 commit 260962e
Show file tree
Hide file tree
Showing 6 changed files with 458 additions and 11 deletions.
1 change: 1 addition & 0 deletions coinlib/lib/src/coinlib_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export 'package:coinlib/src/scripts/programs/p2witness.dart';
export 'package:coinlib/src/scripts/programs/p2wpkh.dart';
export 'package:coinlib/src/scripts/programs/p2wsh.dart';

export 'package:coinlib/src/tx/coin_selection.dart';
export 'package:coinlib/src/tx/transaction.dart';
export 'package:coinlib/src/tx/outpoint.dart';
export 'package:coinlib/src/tx/output.dart';
Expand Down
5 changes: 5 additions & 0 deletions coinlib/lib/src/crypto/random.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ Uint8List generateRandomBytes(int size) {
return bytes;
}

List<T> insertRandom<T>(List<T> list, T element) {
final newList = List<T>.from(list);
newList.insert(Random.secure().nextInt(newList.length+1), element);
return newList;
}
25 changes: 15 additions & 10 deletions coinlib/lib/src/network_params.dart
Original file line number Diff line number Diff line change
@@ -1,42 +1,47 @@

class NetworkParams {

final int wifPrefix;
final int p2pkhPrefix;
final int p2shPrefix;
final int privHDPrefix;
final int pubHDPrefix;
final String bech32Hrp;
final String messagePrefix;
final int wifPrefix, p2pkhPrefix, p2shPrefix, privHDPrefix, pubHDPrefix;
final String bech32Hrp, messagePrefix;
final BigInt minFee, minOutput, feePerKb;

const NetworkParams({
NetworkParams({
required this.wifPrefix,
required this.p2pkhPrefix,
required this.p2shPrefix,
required this.privHDPrefix,
required this.pubHDPrefix,
required this.bech32Hrp,
required this.messagePrefix,
required this.minFee,
required this.minOutput,
required this.feePerKb,
});

static const mainnet = NetworkParams(
static final mainnet = NetworkParams(
wifPrefix: 183,
p2pkhPrefix: 55,
p2shPrefix: 117,
privHDPrefix: 0x0488ade4,
pubHDPrefix: 0x0488b21e,
bech32Hrp: "pc",
messagePrefix: "Peercoin Signed Message:\n",
minFee: BigInt.from(1000),
minOutput: BigInt.from(10000),
feePerKb: BigInt.from(10000),
);

static const testnet = NetworkParams(
static final testnet = NetworkParams(
wifPrefix: 239,
p2pkhPrefix: 111,
p2shPrefix: 196,
privHDPrefix: 0x043587CF,
pubHDPrefix: 0x04358394,
bech32Hrp: "tpc",
messagePrefix: "Peercoin Signed Message:\n",
minFee: BigInt.from(1000),
minOutput: BigInt.from(10000),
feePerKb: BigInt.from(10000),
);

}
153 changes: 153 additions & 0 deletions coinlib/lib/src/tx/coin_selection.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import 'package:coinlib/src/common/serial.dart';
import 'package:coinlib/src/crypto/random.dart';
import 'package:coinlib/src/scripts/program.dart';
import 'inputs/input.dart';
import 'output.dart';
import 'transaction.dart';

class InsufficientFunds implements Exception {}

/// A candidate input to spend a UTXO with the UTXO value
class InputCandidate {
/// Input that can spend the UTXO
final Input input;
/// Value of UTXO to be spent
final BigInt value;
InputCandidate({ required this.input, required this.value });
}

/// Represents a selection of inputs to fund a transaction. If the inputs
/// provide sufficient value to cover the ouputs and fee for a transaction that
/// isn't too large, [ready] shall be true and it is possible to obtain a
/// signable [transaction].
class CoinSelection {

final int version;
final List<InputCandidate> selected;
final List<Output> recipients;
final Program changeProgram;
final BigInt feePerKb;
final BigInt minFee;
final BigInt minChange;
final int locktime;

/// The total value of selected inputs
late final BigInt inputValue;
/// The total value of all recipient outputs
late final BigInt recipientValue;
/// The fee to be paid by the transaction
late final BigInt fee;
/// The value of the change output. This is 0 for a changeless transaction or
/// negative if there aren't enough funds.
late final BigInt changeValue;
/// The maximum size of the transaction after being fully signed
late final int signedSize;

int _sizeGivenChange(int fixedSize, bool includeChange)
=> fixedSize
+ recipients.fold(0, (acc, output) => acc + output.size)
+ (includeChange ? Output.fromProgram(BigInt.zero, changeProgram).size : 0)
+ MeasureWriter.varIntSizeOfInt(
recipients.length + (includeChange ? 1 : 0),
) as int;

BigInt _feeForSize(int size) {
final feeForSize = feePerKb * BigInt.from(size) ~/ BigInt.from(1000);
return feeForSize.compareTo(minFee) > 0 ? feeForSize : minFee;
}

/// Selects all the inputs from [selected] to send to the [recipients] outputs
/// and provide change to the [changeProgram]. The [feePerKb] specifies the
/// required fee in sats per KB with a minimum fee specified with
/// [minFee]. The [minChange] is the minimum allowed change.
CoinSelection({
this.version = Transaction.currentVersion,
required Iterable<InputCandidate> selected,
required Iterable<Output> recipients,
required this.changeProgram,
required this.feePerKb,
required this.minFee,
required this.minChange,
this.locktime = 0,
}) : selected = List.unmodifiable(selected),
recipients = List.unmodifiable(recipients) {

if (selected.any((candidate) => candidate.input.signedSize == null)) {
throw ArgumentError("Cannot select inputs without known max signed size");
}

// Get input and recipient values
inputValue = selected
.fold(BigInt.zero, (acc, candidate) => acc + candidate.value);
recipientValue = recipients
.fold(BigInt.zero, (acc, output) => acc + output.value);

// Get unchanging size
final int fixedSize
// Version and locktime
= 8
// Fully signed inputs
+ MeasureWriter.varIntSizeOfInt(selected.length)
+ selected.fold(0, (acc, candidate) => acc + candidate.input.signedSize!);

// Determine size and fee with change
final sizeWithChange = _sizeGivenChange(fixedSize, true);
final feeWithChange = _feeForSize(sizeWithChange);
final includedChangeValue = inputValue - recipientValue - feeWithChange;

// If change is under the required minimum, remove the change output
if (includedChangeValue.compareTo(minChange) < 0) {

final changelessSize = _sizeGivenChange(fixedSize, false);
final feeForSize = _feeForSize(changelessSize);
final excess = inputValue - recipientValue - feeForSize;

if (!excess.isNegative) {
// Exceeded without change. Fee is the input value minus the recipient
// value
signedSize = changelessSize;
fee = inputValue - recipientValue;
changeValue = BigInt.zero;
return;
}
// Else haven't met requirement

}

// Either haven't met requirement, or have met requirement with change so
// provide details of change-containing transaction
signedSize = sizeWithChange;
fee = feeWithChange;
changeValue = includedChangeValue;

}

/// Obtains the transaction with selected inputs and outputs including any
/// change at a random location, ready to be signed. Throws
/// [InsufficientFunds] if there is not enough input value to meet the output
/// value and fee, or [TransactionTooLarge] if the resulting signed
/// transaction would be too large.
Transaction get transaction {
if (!enoughFunds) throw InsufficientFunds();
if (tooLarge) throw TransactionTooLarge();
return Transaction(
version: version,
inputs: selected.map((candidate) => candidate.input),
outputs: changeless ? recipients : insertRandom(
recipients,
Output.fromProgram(changeValue, changeProgram),
),
locktime: locktime,
);
}

/// True when the input value covers the outputs and fee
bool get enoughFunds => !changeValue.isNegative;
/// True when the change output is omitted
bool get changeless => changeValue.compareTo(BigInt.zero) == 0;
/// True if the resulting fully signed transaction will be too large
bool get tooLarge => signedSize > Transaction.maxSize;
/// True if a signable solution have been found
bool get ready => enoughFunds && !tooLarge;

}
8 changes: 7 additions & 1 deletion coinlib/test/address_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import 'package:coinlib/coinlib.dart';
import 'package:test/test.dart';
import 'vectors/taproot.dart';

const wrongNetwork = NetworkParams(
final wrongNetwork = NetworkParams(
wifPrefix: 0,
p2pkhPrefix: 0xfa,
p2shPrefix: 0xfb,
privHDPrefix: 0,
pubHDPrefix: 0,
bech32Hrp: "wrong",
messagePrefix: "",
feePerKb: BigInt.from(10000),
minFee: BigInt.from(1000),
minOutput: BigInt.from(10000),
);

expectBase58Equal(Base58Address addr, Base58Address expected) {
Expand Down Expand Up @@ -366,6 +369,9 @@ void main() {
wifPrefix: 0, p2shPrefix: 0, p2pkhPrefix: 0,
privHDPrefix: 0, pubHDPrefix: 0,
bech32Hrp: longHrp, messagePrefix: "",
feePerKb: BigInt.from(10000),
minFee: BigInt.from(1000),
minOutput: BigInt.from(10000),
),
addr,
);
Expand Down
Loading

0 comments on commit 260962e

Please sign in to comment.