Commit fe545d5a authored by Sergei Zubov's avatar Sergei Zubov

Modify input selection

Only confirmed coinstake transactions are passed to transaction builder
as available inputs.
Inputs from coinstake transactions are not grouped to avoid spending
all coins avaliable for staking.
parent f9be03bf
using System.Collections.Generic;
using System.Linq;
namespace NBitcoin
{
public class DeStreamCoinSelector : DefaultCoinSelector
{
public Script StakeScript { get; set; }
public DeStreamCoinSelector()
{
}
public DeStreamCoinSelector(int seed) : base(seed)
{
}
public override IEnumerable<ICoin> Select(IEnumerable<ICoin> coins, IMoney target)
{
IMoney zero = target.Sub(target);
var result = new List<ICoin>();
IMoney total = zero;
if (target.CompareTo(zero) == 0)
return result;
// All CoinStake transactions in wallet got the same ScriptPubKey, and grouping them may lead to
// spending all coins available for staking. To avoid this, CoinStake transactions are not grouping
var orderedCoinGroups = coins.GroupBy(c => this.GroupByScriptPubKey
? this.StakeScript != null && c.TxOut.ScriptPubKey == this.StakeScript ? new Key().ScriptPubKey :
c.TxOut.ScriptPubKey
: new Key().ScriptPubKey)
.Select(scriptPubKeyCoins => new
{
Amount = scriptPubKeyCoins.Select(c => c.Amount).Sum(zero),
Coins = scriptPubKeyCoins.ToList()
}).OrderBy(c => c.Amount).ToList();
var targetCoin = orderedCoinGroups
.FirstOrDefault(c => c.Amount.CompareTo(target) == 0);
//If any of your UTXO² matches the Target¹ it will be used.
if (targetCoin != null)
return targetCoin.Coins;
foreach (var coinGroup in orderedCoinGroups)
{
if (coinGroup.Amount.CompareTo(target) == -1 && total.CompareTo(target) == -1)
{
total = total.Add(coinGroup.Amount);
result.AddRange(coinGroup.Coins);
//If the "sum of all your UTXO smaller than the Target" happens to match the Target, they will be used. (This is the case if you sweep a complete wallet.)
if (total.CompareTo(target) == 0)
return result;
}
else
{
if (total.CompareTo(target) == -1 && coinGroup.Amount.CompareTo(target) == 1)
{
//If the "sum of all your UTXO smaller than the Target" doesn't surpass the target, the smallest UTXO greater than your Target will be used.
return coinGroup.Coins;
}
// Else Bitcoin Core does 1000 rounds of randomly combining unspent transaction outputs until their sum is greater than or equal to the Target. If it happens to find an exact match, it stops early and uses that.
//Otherwise it finally settles for the minimum of
//the smallest UTXO greater than the Target
//the smallest combination of UTXO it discovered in Step 4.
var allCoins = orderedCoinGroups.ToArray();
IMoney minTotal = null;
for (int _ = 0; _ < 1000; _++)
{
var selection = new List<ICoin>();
Utils.Shuffle(allCoins, this._Rand);
total = zero;
foreach (var coin in allCoins)
{
selection.AddRange(coin.Coins);
total = total.Add(coin.Amount);
if (total.CompareTo(target) == 0)
return selection;
if (total.CompareTo(target) == 1)
break;
}
if (total.CompareTo(target) == -1) return null;
if (minTotal == null || total.CompareTo(minTotal) == -1) minTotal = total;
}
}
}
return total.CompareTo(target) == -1 ? null : result;
}
}
}
\ No newline at end of file
...@@ -9,10 +9,12 @@ namespace NBitcoin ...@@ -9,10 +9,12 @@ namespace NBitcoin
{ {
public DeStreamTransactionBuilder(Network network) : base(network) public DeStreamTransactionBuilder(Network network) : base(network)
{ {
this.CoinSelector = new DeStreamCoinSelector();
} }
public DeStreamTransactionBuilder(int seed, Network network) : base(seed, network) public DeStreamTransactionBuilder(int seed, Network network) : base(seed, network)
{ {
this.CoinSelector = new DeStreamCoinSelector(seed);
} }
protected override IEnumerable<ICoin> BuildTransaction(TransactionBuildingContext ctx, BuilderGroup group, protected override IEnumerable<ICoin> BuildTransaction(TransactionBuildingContext ctx, BuilderGroup group,
......
...@@ -34,7 +34,7 @@ namespace NBitcoin ...@@ -34,7 +34,7 @@ namespace NBitcoin
} }
private Random _Rand = new Random(); protected Random _Rand = new Random();
public DefaultCoinSelector(int seed) public DefaultCoinSelector(int seed)
{ {
this._Rand = new Random(seed); this._Rand = new Random(seed);
...@@ -50,7 +50,7 @@ namespace NBitcoin ...@@ -50,7 +50,7 @@ namespace NBitcoin
#region ICoinSelector Members #region ICoinSelector Members
public IEnumerable<ICoin> Select(IEnumerable<ICoin> coins, IMoney target) public virtual IEnumerable<ICoin> Select(IEnumerable<ICoin> coins, IMoney target)
{ {
IMoney zero = target.Sub(target); IMoney zero = target.Sub(target);
......
...@@ -5,6 +5,7 @@ using System.Security; ...@@ -5,6 +5,7 @@ using System.Security;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using Stratis.Bitcoin.Features.Consensus;
using Stratis.Bitcoin.Features.Wallet.Interfaces; using Stratis.Bitcoin.Features.Wallet.Interfaces;
using Stratis.Bitcoin.Utilities; using Stratis.Bitcoin.Utilities;
using Stratis.Bitcoin.Utilities.Extensions; using Stratis.Bitcoin.Utilities.Extensions;
...@@ -126,6 +127,91 @@ namespace Stratis.Bitcoin.Features.Wallet ...@@ -126,6 +127,91 @@ namespace Stratis.Bitcoin.Features.Wallet
context.TransactionBuilder.AddKeys(signingKeys.ToArray()); context.TransactionBuilder.AddKeys(signingKeys.ToArray());
} }
/// <inheritdoc />
protected override void AddCoins(TransactionBuildContext context)
{
// Transaction.IsCoinStake == true - for coinstake transactions
// Transaction.IsCoinStake == null - for non-coinstake transactions
// Transaction.IsCoinStake == false - not encountered (bug?)
// Take all non-coinstake transactions
context.UnspentOutputs = this.walletManager
.GetSpendableTransactionsInAccount(context.AccountReference, context.MinConfirmations)
.Where(p => p.Transaction.IsCoinStake != true)
.ToList();
// Add all coinstake transactions with enough confirmations
context.UnspentOutputs.AddRange(this.walletManager
.GetSpendableTransactionsInAccount(context.AccountReference,
this.Network.Consensus.Option<PosConsensusOptions>()
.GetStakeMinConfirmations(this.walletManager.LastBlockHeight(), this.Network))
.Where(p => p.Transaction.IsCoinStake ?? false)
.ToList());
(context.TransactionBuilder.CoinSelector as DeStreamCoinSelector ?? throw new NotSupportedException(
$"{nameof(context.TransactionBuilder.CoinSelector)} must be {typeof(DeStreamCoinSelector)} type"))
.StakeScript = context.UnspentOutputs
.FirstOrDefault(p => p.Transaction.IsCoinStake ?? false)
?.Address.Pubkey;
if (context.UnspentOutputs.Count == 0) throw new WalletException("No spendable transactions found.");
// Get total spendable balance in the account.
long balance = context.UnspentOutputs.Sum(t => t.Transaction.Amount);
long totalToSend = context.Recipients.Sum(s => s.Amount);
if (balance < totalToSend)
throw new WalletException("Not enough funds.");
if (context.SelectedInputs.Any())
{
// 'SelectedInputs' are inputs that must be included in the
// current transaction. At this point we check the given
// input is part of the UTXO set and filter out UTXOs that are not
// in the initial list if 'context.AllowOtherInputs' is false.
Dictionary<OutPoint, UnspentOutputReference> availableHashList =
context.UnspentOutputs.ToDictionary(item => item.ToOutPoint(), item => item);
if (!context.SelectedInputs.All(input => availableHashList.ContainsKey(input)))
throw new WalletException("Not all the selected inputs were found on the wallet.");
if (!context.AllowOtherInputs)
{
foreach (KeyValuePair<OutPoint, UnspentOutputReference> unspentOutputsItem in availableHashList)
{
if (!context.SelectedInputs.Contains(unspentOutputsItem.Key))
context.UnspentOutputs.Remove(unspentOutputsItem.Value);
}
}
}
Money sum = 0;
int index = 0;
var coins = new List<Coin>();
foreach (UnspentOutputReference item in context.UnspentOutputs.OrderByDescending(a => a.Transaction.Amount))
{
coins.Add(new Coin(item.Transaction.Id, (uint) item.Transaction.Index, item.Transaction.Amount,
item.Transaction.ScriptPubKey));
sum += item.Transaction.Amount;
index++;
// If threshold is reached and the total value is above the target
// then its safe to stop adding UTXOs to the coin list.
// The primery goal is to reduce the time it takes to build a trx
// when the wallet is bloated with UTXOs.
if (index > SendCountThresholdLimit && sum > totalToSend)
break;
}
// All the UTXOs are added to the builder without filtering.
// The builder then has its own coin selection mechanism
// to select the best UTXO set for the corresponding amount.
// To add a custom implementation of a coin selection override
// the builder using builder.SetCoinSelection().
context.TransactionBuilder.AddCoins(coins);
}
/// <inheritdoc /> /// <inheritdoc />
protected override void InitializeTransactionBuilder(TransactionBuildContext context) protected override void InitializeTransactionBuilder(TransactionBuildContext context)
{ {
......
...@@ -31,7 +31,7 @@ namespace Stratis.Bitcoin.Features.Wallet ...@@ -31,7 +31,7 @@ namespace Stratis.Bitcoin.Features.Wallet
/// 500 is a safe number that if reached ensures the coin selector will not take too long to complete, /// 500 is a safe number that if reached ensures the coin selector will not take too long to complete,
/// most regular wallets will never reach such a high number of UTXO. /// most regular wallets will never reach such a high number of UTXO.
/// </remarks> /// </remarks>
private const int SendCountThresholdLimit = 500; protected const int SendCountThresholdLimit = 500;
protected readonly IWalletManager walletManager; protected readonly IWalletManager walletManager;
...@@ -264,7 +264,7 @@ namespace Stratis.Bitcoin.Features.Wallet ...@@ -264,7 +264,7 @@ namespace Stratis.Bitcoin.Features.Wallet
/// Then add them to the <see cref="TransactionBuildContext.UnspentOutputs"/>. /// Then add them to the <see cref="TransactionBuildContext.UnspentOutputs"/>.
/// </summary> /// </summary>
/// <param name="context">The context associated with the current transaction being built.</param> /// <param name="context">The context associated with the current transaction being built.</param>
protected void AddCoins(TransactionBuildContext context) protected virtual void AddCoins(TransactionBuildContext context)
{ {
context.UnspentOutputs = this.walletManager.GetSpendableTransactionsInAccount(context.AccountReference, context.MinConfirmations).ToList(); context.UnspentOutputs = this.walletManager.GetSpendableTransactionsInAccount(context.AccountReference, context.MinConfirmations).ToList();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment