utxo grouping, filtering and zero conf handling

This commit is contained in:
Craig Raw 2020-07-15 09:15:27 +02:00
parent 832ca8f257
commit f00754b157
5 changed files with 136 additions and 32 deletions

View file

@ -32,20 +32,20 @@ public class BlockTransaction extends BlockTransactionHash implements Comparable
} }
@Override @Override
public int compareTo(BlockTransaction blockchainTransaction) { public int compareTo(BlockTransaction blkTx) {
if(getHeight() != blockchainTransaction.getHeight()) { if(getHeight() != blkTx.getHeight()) {
return getHeight() - blockchainTransaction.getHeight(); return (getHeight() > 0 ? getHeight() : Integer.MAX_VALUE) - (blkTx.getHeight() > 0 ? blkTx.getHeight() : Integer.MAX_VALUE);
} }
if(getReferencedOutpoints(this).removeAll(getOutputs(blockchainTransaction))) { if(getReferencedOutpoints(this).removeAll(getOutputs(blkTx))) {
return 1; return 1;
} }
if(getReferencedOutpoints(blockchainTransaction).removeAll(getOutputs(this))) { if(getReferencedOutpoints(blkTx).removeAll(getOutputs(this))) {
return -1; return -1;
} }
return super.compareTo(blockchainTransaction); return super.compareTo(blkTx);
} }
private static List<HashIndex> getReferencedOutpoints(BlockTransaction blockchainTransaction) { private static List<HashIndex> getReferencedOutpoints(BlockTransaction blockchainTransaction) {

View file

@ -67,9 +67,8 @@ public abstract class BlockTransactionHash {
} }
public int compareTo(BlockTransactionHash reference) { public int compareTo(BlockTransactionHash reference) {
int heightDiff = height - reference.height; if(height != reference.height) {
if(heightDiff != 0) { return (height > 0 ? height : Integer.MAX_VALUE) - (reference.height > 0 ? reference.height : Integer.MAX_VALUE);
return heightDiff;
} }
return hash.compareTo(reference.hash); return hash.compareTo(reference.hash);

View file

@ -7,6 +7,7 @@ import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR
public class OutputGroup { public class OutputGroup {
private final List<BlockTransactionHashIndex> utxos = new ArrayList<>(); private final List<BlockTransactionHashIndex> utxos = new ArrayList<>();
private final int walletBlockHeight;
private final long inputWeightUnits; private final long inputWeightUnits;
private final double feeRate; private final double feeRate;
private final double longTermFeeRate; private final double longTermFeeRate;
@ -14,26 +15,24 @@ public class OutputGroup {
private long effectiveValue = 0; private long effectiveValue = 0;
private long fee = 0; private long fee = 0;
private long longTermFee = 0; private long longTermFee = 0;
private int depth = Integer.MAX_VALUE;
private boolean allInputsFromWallet = true;
public OutputGroup(long inputWeightUnits, double feeRate, double longTermFeeRate) { public OutputGroup(int walletBlockHeight, long inputWeightUnits, double feeRate, double longTermFeeRate) {
this.walletBlockHeight = walletBlockHeight;
this.inputWeightUnits = inputWeightUnits; this.inputWeightUnits = inputWeightUnits;
this.feeRate = feeRate; this.feeRate = feeRate;
this.longTermFeeRate = longTermFeeRate; this.longTermFeeRate = longTermFeeRate;
} }
public OutputGroup(long inputWeightUnits, double feeRate, double longTermFeeRate, BlockTransactionHashIndex utxo) { public void add(BlockTransactionHashIndex utxo, boolean allInputsFromWallet) {
this.inputWeightUnits = inputWeightUnits;
this.feeRate = feeRate;
this.longTermFeeRate = longTermFeeRate;
add(utxo);
}
public void add(BlockTransactionHashIndex utxo) {
utxos.add(utxo); utxos.add(utxo);
value += utxo.getValue(); value += utxo.getValue();
effectiveValue += utxo.getValue() - (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR); effectiveValue += utxo.getValue() - (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR);
fee += (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR); fee += (long)(inputWeightUnits * feeRate / WITNESS_SCALE_FACTOR);
longTermFee += (long)(inputWeightUnits * longTermFeeRate / WITNESS_SCALE_FACTOR); longTermFee += (long)(inputWeightUnits * longTermFeeRate / WITNESS_SCALE_FACTOR);
depth = utxo.getHeight() <= 0 ? 0 : Math.min(depth, walletBlockHeight - utxo.getHeight() + 1);
this.allInputsFromWallet &= allInputsFromWallet;
} }
public void remove(BlockTransactionHashIndex utxo) { public void remove(BlockTransactionHashIndex utxo) {
@ -64,4 +63,30 @@ public class OutputGroup {
public long getLongTermFee() { public long getLongTermFee() {
return longTermFee; return longTermFee;
} }
public int getDepth() {
return depth;
}
public boolean isAllInputsFromWallet() {
return allInputsFromWallet;
}
public static class Filter {
private final int minWalletConfirmations;
private final int minExternalConfirmations;
public Filter(int minWalletConfirmations, int minExternalConfirmations) {
this.minWalletConfirmations = minWalletConfirmations;
this.minExternalConfirmations = minExternalConfirmations;
}
public boolean isEligible(OutputGroup outputGroup) {
if(outputGroup.isAllInputsFromWallet()) {
return outputGroup.getDepth() >= minWalletConfirmations;
}
return outputGroup.getDepth() >= minExternalConfirmations;
}
}
} }

View file

@ -15,7 +15,7 @@ public class PriorityUtxoSelector implements UtxoSelector {
public Collection<BlockTransactionHashIndex> select(long targetValue, Collection<OutputGroup> candidates) { public Collection<BlockTransactionHashIndex> select(long targetValue, Collection<OutputGroup> candidates) {
List<BlockTransactionHashIndex> selected = new ArrayList<>(); List<BlockTransactionHashIndex> selected = new ArrayList<>();
List<BlockTransactionHashIndex> sorted = candidates.stream().flatMap(outputGroup -> outputGroup.getUtxos().stream()).filter(ref -> ref.getHeight() != 0).collect(Collectors.toList()); List<BlockTransactionHashIndex> sorted = candidates.stream().flatMap(outputGroup -> outputGroup.getUtxos().stream()).filter(ref -> ref.getHeight() > 0).collect(Collectors.toList());
sort(sorted); sort(sorted);
//Testing only: remove //Testing only: remove

View file

@ -233,12 +233,29 @@ public class Wallet {
} }
} }
public boolean isWalletTxo(BlockTransactionHashIndex txo) {
return getWalletTxos().containsKey(txo);
}
public Map<BlockTransactionHashIndex, WalletNode> getWalletTxos() {
Map<BlockTransactionHashIndex, WalletNode> walletTxos = new TreeMap<>();
getWalletTxos(walletTxos, getNode(KeyPurpose.RECEIVE));
getWalletTxos(walletTxos, getNode(KeyPurpose.CHANGE));
return walletTxos;
}
private void getWalletTxos(Map<BlockTransactionHashIndex, WalletNode> walletTxos, WalletNode purposeNode) {
for(WalletNode addressNode : purposeNode.getChildren()) {
for(BlockTransactionHashIndex txo : addressNode.getTransactionOutputs()) {
walletTxos.put(txo, addressNode);
}
}
}
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos() { public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos() {
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>(); Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>();
getWalletUtxos(walletUtxos, getNode(KeyPurpose.RECEIVE)); getWalletUtxos(walletUtxos, getNode(KeyPurpose.RECEIVE));
getWalletUtxos(walletUtxos, getNode(KeyPurpose.CHANGE)); getWalletUtxos(walletUtxos, getNode(KeyPurpose.CHANGE));
return walletUtxos; return walletUtxos;
} }
@ -357,11 +374,11 @@ public class Wallet {
return getFee(changeOutput, feeRate, longTermFeeRate); return getFee(changeOutput, feeRate, longTermFeeRate);
} }
public WalletTransaction createWalletTransaction(List<UtxoSelector> utxoSelectors, Address recipientAddress, long recipientAmount, double feeRate, double longTermFeeRate, Long fee, boolean sendAll) throws InsufficientFundsException { public WalletTransaction createWalletTransaction(List<UtxoSelector> utxoSelectors, Address recipientAddress, long recipientAmount, double feeRate, double longTermFeeRate, Long fee, boolean sendAll, boolean groupByAddress, boolean includeMempoolChange) throws InsufficientFundsException {
long valueRequiredAmt = recipientAmount; long valueRequiredAmt = recipientAmount;
while(true) { while(true) {
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = selectInputs(utxoSelectors, valueRequiredAmt, feeRate, longTermFeeRate); Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = selectInputs(utxoSelectors, valueRequiredAmt, feeRate, longTermFeeRate, groupByAddress, includeMempoolChange);
long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(); long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
//Add inputs //Add inputs
@ -434,22 +451,85 @@ public class Wallet {
} }
} }
private Map<BlockTransactionHashIndex, WalletNode> selectInputs(List<UtxoSelector> utxoSelectors, Long targetValue, double feeRate, double longTermFeeRate) throws InsufficientFundsException { private Map<BlockTransactionHashIndex, WalletNode> selectInputs(List<UtxoSelector> utxoSelectors, Long targetValue, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeMempoolChange) throws InsufficientFundsException {
Map<BlockTransactionHashIndex, WalletNode> utxos = getWalletUtxos(); List<OutputGroup> utxoPool = getGroupedUtxos(feeRate, longTermFeeRate, groupByAddress);
for(UtxoSelector utxoSelector : utxoSelectors) { List<OutputGroup.Filter> filters = new ArrayList<>();
List<OutputGroup> utxoPool = utxos.keySet().stream().map(utxo -> new OutputGroup(getInputWeightUnits(), feeRate, longTermFeeRate, utxo)).collect(Collectors.toList()); filters.add(new OutputGroup.Filter(1, 6));
Collection<BlockTransactionHashIndex> selectedInputs = utxoSelector.select(targetValue, utxoPool); filters.add(new OutputGroup.Filter(1, 1));
long total = selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum(); if(includeMempoolChange) {
if(total > targetValue) { filters.add(new OutputGroup.Filter(0, 1));
utxos.keySet().retainAll(selectedInputs); }
return utxos;
for(OutputGroup.Filter filter : filters) {
List<OutputGroup> filteredPool = utxoPool.stream().filter(filter::isEligible).collect(Collectors.toList());
for(UtxoSelector utxoSelector : utxoSelectors) {
Collection<BlockTransactionHashIndex> selectedInputs = utxoSelector.select(targetValue, filteredPool);
long total = selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
if(total > targetValue) {
Map<BlockTransactionHashIndex, WalletNode> utxos = getWalletUtxos();
utxos.keySet().retainAll(selectedInputs);
return utxos;
}
} }
} }
throw new InsufficientFundsException("Not enough combined value in UTXOs for output value " + targetValue); throw new InsufficientFundsException("Not enough combined value in UTXOs for output value " + targetValue);
} }
private List<OutputGroup> getGroupedUtxos(double feeRate, double longTermFeeRate, boolean groupByAddress) {
List<OutputGroup> outputGroups = new ArrayList<>();
getGroupedUtxos(outputGroups, getNode(KeyPurpose.RECEIVE), feeRate, longTermFeeRate, groupByAddress);
getGroupedUtxos(outputGroups, getNode(KeyPurpose.CHANGE), feeRate, longTermFeeRate, groupByAddress);
return outputGroups;
}
private void getGroupedUtxos(List<OutputGroup> outputGroups, WalletNode purposeNode, double feeRate, double longTermFeeRate, boolean groupByAddress) {
for(WalletNode addressNode : purposeNode.getChildren()) {
OutputGroup outputGroup = null;
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs()) {
if(outputGroup == null || !groupByAddress) {
outputGroup = new OutputGroup(getStoredBlockHeight(), getInputWeightUnits(), feeRate, longTermFeeRate);
outputGroups.add(outputGroup);
}
outputGroup.add(utxo, allInputsFromWallet(utxo.getHash()));
}
}
}
/**
* Determines if the provided wallet transaction was created from a purely internal transaction
*
* @param txId The txid
* @return Whether the transaction was created entirely from inputs that reference outputs that belong to this wallet
*/
private boolean allInputsFromWallet(Sha256Hash txId) {
BlockTransaction utxoBlkTx = getTransactions().get(txId);
if(utxoBlkTx == null) {
throw new IllegalArgumentException("Provided txId was not a wallet transaction");
}
for(int i = 0; i < utxoBlkTx.getTransaction().getInputs().size(); i++) {
TransactionInput utxoTxInput = utxoBlkTx.getTransaction().getInputs().get(i);
BlockTransaction prevBlkTx = getTransactions().get(utxoTxInput.getOutpoint().getHash());
if(prevBlkTx == null) {
return false;
}
int index = (int)utxoTxInput.getOutpoint().getIndex();
TransactionOutput prevTxOut = prevBlkTx.getTransaction().getOutputs().get(index);
BlockTransactionHashIndex spendingTXI = new BlockTransactionHashIndex(utxoBlkTx.getHash(), utxoBlkTx.getHeight(), utxoBlkTx.getDate(), utxoBlkTx.getFee(), i, prevTxOut.getValue());
BlockTransactionHashIndex spentTXO = new BlockTransactionHashIndex(prevBlkTx.getHash(), prevBlkTx.getHeight(), prevBlkTx.getDate(), prevBlkTx.getFee(), index, prevTxOut.getValue(), spendingTXI);
if(!isWalletTxo(spentTXO)) {
return false;
}
}
return true;
}
public BitcoinUnit getAutoUnit() { public BitcoinUnit getAutoUnit() {
for(KeyPurpose keyPurpose : KeyPurpose.values()) { for(KeyPurpose keyPurpose : KeyPurpose.values()) {
for(WalletNode addressNode : getNode(keyPurpose).getChildren()) { for(WalletNode addressNode : getNode(keyPurpose).getChildren()) {