Commit e0951510 authored by Sergei Zubov's avatar Sergei Zubov

Add mark at output with change

To secure that fee is charged from spending coins and not from change,
DeStream marks output with change.
It is implemented via additional empty input with PrevOut hash
uint256.Zero, that points to output with change. Input is signed, so
it's verified that user has access to address from output, and this is
change address.
parent 44597e7a
using System;
using System.Collections.Generic;
using System.Linq;
using NBitcoin.Policy;
namespace NBitcoin
{
public class DeStreamTransactionBuilder : TransactionBuilder
{
public DeStreamTransactionBuilder(Network network) : base(network)
{
}
public DeStreamTransactionBuilder(int seed, Network network) : base(seed, network)
{
}
protected override IEnumerable<ICoin> BuildTransaction(TransactionBuildingContext ctx, BuilderGroup group,
IEnumerable<Func<TransactionBuildingContext, IMoney>> builders,
IEnumerable<ICoin> coins, IMoney zero)
{
IEnumerable<ICoin> result = base.BuildTransaction(ctx, group, builders, coins, zero);
if(ctx.Transaction.Inputs.Any(p => p.PrevOut.Hash == uint256.Zero))
return result;
// To secure that fee is charged from spending coins and not from change,
// we add input with uint256.Zero hash that points to output with change
var outPoint = new OutPoint
{
Hash = uint256.Zero,
N = (uint) ctx.Transaction.Outputs.FindIndex(p =>
p.ScriptPubKey == group.ChangeScript[(int) ctx.ChangeType])
};
ctx.Transaction.AddInput(new TxIn
{
PrevOut = outPoint
});
group.Coins.Add(outPoint, new Coin(uint256.Zero, outPoint.N,
Money.Zero, group.ChangeScript[(int) ctx.ChangeType]));
return result;
}
}
}
\ No newline at end of file
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
...@@ -318,7 +318,7 @@ namespace NBitcoin ...@@ -318,7 +318,7 @@ namespace NBitcoin
} }
} }
internal class TransactionBuildingContext public class TransactionBuildingContext
{ {
public TransactionBuildingContext(TransactionBuilder builder) public TransactionBuildingContext(TransactionBuilder builder)
{ {
...@@ -460,7 +460,7 @@ namespace NBitcoin ...@@ -460,7 +460,7 @@ namespace NBitcoin
} }
} }
internal class BuilderGroup public class BuilderGroup
{ {
private TransactionBuilder _Parent; private TransactionBuilder _Parent;
public BuilderGroup(TransactionBuilder parent) public BuilderGroup(TransactionBuilder parent)
...@@ -867,7 +867,7 @@ namespace NBitcoin ...@@ -867,7 +867,7 @@ namespace NBitcoin
return this; return this;
} }
private Money GetDust() protected Money GetDust()
{ {
return GetDust(new Script(new byte[25])); return GetDust(new Script(new byte[25]));
} }
...@@ -1152,7 +1152,7 @@ namespace NBitcoin ...@@ -1152,7 +1152,7 @@ namespace NBitcoin
return c.Amount >= this.FilterUneconomicalCoinsRate.GetFee(vSize); return c.Amount >= this.FilterUneconomicalCoinsRate.GetFee(vSize);
} }
private IEnumerable<ICoin> BuildTransaction( protected virtual IEnumerable<ICoin> BuildTransaction(
TransactionBuildingContext ctx, TransactionBuildingContext ctx,
BuilderGroup group, BuilderGroup group,
IEnumerable<Builder> builders, IEnumerable<Builder> builders,
......
...@@ -237,5 +237,29 @@ namespace Stratis.Bitcoin.Features.Miner ...@@ -237,5 +237,29 @@ namespace Stratis.Bitcoin.Features.Miner
this.logger.LogTrace("Coinstake UTXO will be split to two."); this.logger.LogTrace("Coinstake UTXO will be split to two.");
outputs.Insert(2, new TxOut(0, outputs[1].ScriptPubKey)); outputs.Insert(2, new TxOut(0, outputs[1].ScriptPubKey));
} }
protected override bool SignTransactionInput(UtxoStakeDescription input, Transaction transaction)
{
this.logger.LogTrace("({0}:'{1}')", nameof(input), input.OutPoint);
bool res = false;
try
{
new DeStreamTransactionBuilder(this.network)
.AddKeys(input.Key)
.AddCoins(new Coin(input.OutPoint, input.TxOut))
.SignTransactionInPlace(transaction);
res = true;
}
catch (Exception e)
{
this.logger.LogDebug("Exception occurred: {0}", e.ToString());
}
this.logger.LogTrace("(-):{0}", res);
return res;
}
} }
} }
\ No newline at end of file
...@@ -971,7 +971,7 @@ namespace Stratis.Bitcoin.Features.Miner ...@@ -971,7 +971,7 @@ namespace Stratis.Bitcoin.Features.Miner
/// <param name="input">Transaction input.</param> /// <param name="input">Transaction input.</param>
/// <param name="transaction">Transaction being built.</param> /// <param name="transaction">Transaction being built.</param>
/// <returns><c>true</c> if the function succeeds, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if the function succeeds, <c>false</c> otherwise.</returns>
protected bool SignTransactionInput(UtxoStakeDescription input, Transaction transaction) protected virtual bool SignTransactionInput(UtxoStakeDescription input, Transaction transaction)
{ {
this.logger.LogTrace("({0}:'{1}')", nameof(input), input.OutPoint); this.logger.LogTrace("({0}:'{1}')", nameof(input), input.OutPoint);
......
using System.Linq; using System;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin; using NBitcoin;
using Stratis.Bitcoin.Features.Wallet.Interfaces; using Stratis.Bitcoin.Features.Wallet.Interfaces;
using Stratis.Bitcoin.Utilities;
using Stratis.Bitcoin.Utilities.Extensions;
namespace Stratis.Bitcoin.Features.Wallet namespace Stratis.Bitcoin.Features.Wallet
{ {
...@@ -20,5 +26,121 @@ namespace Stratis.Bitcoin.Features.Wallet ...@@ -20,5 +26,121 @@ namespace Stratis.Bitcoin.Features.Wallet
context.TransactionFee = fee; context.TransactionFee = fee;
context.TransactionBuilder.SendFees(fee); context.TransactionBuilder.SendFees(fee);
} }
/// <inheritdoc />
public override (Money maximumSpendableAmount, Money Fee) GetMaximumSpendableAmount(
WalletAccountReference accountReference,
FeeType feeType, bool allowUnconfirmed)
{
Guard.NotNull(accountReference, nameof(accountReference));
Guard.NotEmpty(accountReference.WalletName, nameof(accountReference.WalletName));
Guard.NotEmpty(accountReference.AccountName, nameof(accountReference.AccountName));
// Get the total value of spendable coins in the account.
long maxSpendableAmount = this.walletManager
.GetSpendableTransactionsInAccount(accountReference, allowUnconfirmed ? 0 : 1)
.Sum(x => x.Transaction.Amount);
// Return 0 if the user has nothing to spend.
if (maxSpendableAmount == Money.Zero) return (Money.Zero, Money.Zero);
// Create a recipient with a dummy destination address as it's required by NBitcoin's transaction builder.
List<Recipient> recipients = new[]
{new Recipient {Amount = new Money(maxSpendableAmount), ScriptPubKey = new Key().ScriptPubKey}}
.ToList();
Money fee;
try
{
// Here we try to create a transaction that contains all the spendable coins, leaving no room for the fee.
// When the transaction builder throws an exception informing us that we have insufficient funds,
// we use the amount we're missing as the fee.
var context = new TransactionBuildContext(accountReference, recipients, null)
{
FeeType = feeType,
MinConfirmations = allowUnconfirmed ? 0 : 1,
TransactionBuilder = new DeStreamTransactionBuilder(this.Network)
};
this.AddRecipients(context);
this.AddCoins(context);
this.AddFee(context);
// Throw an exception if this code is reached, as building a transaction without any funds for the fee should always throw an exception.
throw new WalletException(
"This should be unreachable; please find and fix the bug that caused this to be reached.");
}
catch (NotEnoughFundsException e)
{
fee = (Money) e.Missing;
}
return (maxSpendableAmount - fee, fee);
}
/// <inheritdoc />
protected override void AddSecrets(TransactionBuildContext context)
{
if (!context.Sign)
return;
Wallet wallet = this.walletManager.GetWalletByName(context.AccountReference.WalletName);
Key privateKey;
// get extended private key
string cacheKey = wallet.EncryptedSeed;
if (this.privateKeyCache.TryGetValue(cacheKey, out SecureString secretValue))
{
privateKey = wallet.Network.CreateBitcoinSecret(secretValue.FromSecureString()).PrivateKey;
this.privateKeyCache.Set(cacheKey, secretValue, new TimeSpan(0, 5, 0));
}
else
{
privateKey = Key.Parse(wallet.EncryptedSeed, context.WalletPassword, wallet.Network);
this.privateKeyCache.Set(cacheKey, privateKey.ToString(wallet.Network).ToSecureString(),
new TimeSpan(0, 5, 0));
}
var seedExtKey = new ExtKey(privateKey, wallet.ChainCode);
var signingKeys = new HashSet<ISecret>();
var added = new HashSet<HdAddress>();
foreach (UnspentOutputReference unspentOutputsItem in context.UnspentOutputs)
{
if (added.Contains(unspentOutputsItem.Address))
continue;
HdAddress address = unspentOutputsItem.Address;
ExtKey addressExtKey = seedExtKey.Derive(new KeyPath(address.HdPath));
BitcoinExtKey addressPrivateKey = addressExtKey.GetWif(wallet.Network);
signingKeys.Add(addressPrivateKey);
added.Add(unspentOutputsItem.Address);
}
// To secure that fee is charged from spending coins and not from change,
// we add empty input with change address, so private key for change address is required.
BitcoinExtKey changeAddressPrivateKey =
seedExtKey.Derive(new KeyPath(context.ChangeAddress.HdPath)).GetWif(wallet.Network);
signingKeys.Add(changeAddressPrivateKey);
context.TransactionBuilder.AddKeys(signingKeys.ToArray());
}
/// <inheritdoc />
protected override void InitializeTransactionBuilder(TransactionBuildContext context)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(context.Recipients, nameof(context.Recipients));
Guard.NotNull(context.AccountReference, nameof(context.AccountReference));
context.TransactionBuilder = new DeStreamTransactionBuilder(this.Network);
this.AddRecipients(context);
this.AddOpReturnOutput(context);
this.AddCoins(context);
this.FindChangeAddress(context);
this.AddSecrets(context);
this.AddFee(context);
}
} }
} }
\ No newline at end of file
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security; using System.Security;
...@@ -33,7 +33,7 @@ namespace Stratis.Bitcoin.Features.Wallet ...@@ -33,7 +33,7 @@ namespace Stratis.Bitcoin.Features.Wallet
/// </remarks> /// </remarks>
private const int SendCountThresholdLimit = 500; private const int SendCountThresholdLimit = 500;
private readonly IWalletManager walletManager; protected readonly IWalletManager walletManager;
private readonly IWalletFeePolicy walletFeePolicy; private readonly IWalletFeePolicy walletFeePolicy;
...@@ -41,7 +41,7 @@ namespace Stratis.Bitcoin.Features.Wallet ...@@ -41,7 +41,7 @@ namespace Stratis.Bitcoin.Features.Wallet
private readonly ILogger logger; private readonly ILogger logger;
private readonly MemoryCache privateKeyCache; protected readonly MemoryCache privateKeyCache;
public WalletTransactionHandler( public WalletTransactionHandler(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
...@@ -130,7 +130,7 @@ namespace Stratis.Bitcoin.Features.Wallet ...@@ -130,7 +130,7 @@ namespace Stratis.Bitcoin.Features.Wallet
} }
/// <inheritdoc /> /// <inheritdoc />
public (Money maximumSpendableAmount, Money Fee) GetMaximumSpendableAmount(WalletAccountReference accountReference, FeeType feeType, bool allowUnconfirmed) public virtual (Money maximumSpendableAmount, Money Fee) GetMaximumSpendableAmount(WalletAccountReference accountReference, FeeType feeType, bool allowUnconfirmed)
{ {
Guard.NotNull(accountReference, nameof(accountReference)); Guard.NotNull(accountReference, nameof(accountReference));
Guard.NotEmpty(accountReference.WalletName, nameof(accountReference.WalletName)); Guard.NotEmpty(accountReference.WalletName, nameof(accountReference.WalletName));
...@@ -188,7 +188,7 @@ namespace Stratis.Bitcoin.Features.Wallet ...@@ -188,7 +188,7 @@ namespace Stratis.Bitcoin.Features.Wallet
/// Initializes the context transaction builder from information in <see cref="TransactionBuildContext"/>. /// Initializes the context transaction builder from information in <see cref="TransactionBuildContext"/>.
/// </summary> /// </summary>
/// <param name="context">Transaction build context.</param> /// <param name="context">Transaction build context.</param>
private void InitializeTransactionBuilder(TransactionBuildContext context) protected virtual void InitializeTransactionBuilder(TransactionBuildContext context)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
Guard.NotNull(context.Recipients, nameof(context.Recipients)); Guard.NotNull(context.Recipients, nameof(context.Recipients));
...@@ -208,7 +208,7 @@ namespace Stratis.Bitcoin.Features.Wallet ...@@ -208,7 +208,7 @@ namespace Stratis.Bitcoin.Features.Wallet
/// Load's all the private keys for each of the <see cref="HdAddress"/> in <see cref="TransactionBuildContext.UnspentOutputs"/> /// Load's all the private keys for each of the <see cref="HdAddress"/> in <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>
private void AddSecrets(TransactionBuildContext context) protected virtual void AddSecrets(TransactionBuildContext context)
{ {
if (!context.Sign) if (!context.Sign)
return; return;
...@@ -252,7 +252,7 @@ namespace Stratis.Bitcoin.Features.Wallet ...@@ -252,7 +252,7 @@ namespace Stratis.Bitcoin.Features.Wallet
/// Find the next available change address. /// Find the next available change address.
/// </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>
private void FindChangeAddress(TransactionBuildContext context) protected void FindChangeAddress(TransactionBuildContext context)
{ {
// Get an address to send the change to. // Get an address to send the change to.
context.ChangeAddress = this.walletManager.GetUnusedChangeAddress(new WalletAccountReference(context.AccountReference.WalletName, context.AccountReference.AccountName)); context.ChangeAddress = this.walletManager.GetUnusedChangeAddress(new WalletAccountReference(context.AccountReference.WalletName, context.AccountReference.AccountName));
...@@ -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>
private void AddCoins(TransactionBuildContext context) protected void AddCoins(TransactionBuildContext context)
{ {
context.UnspentOutputs = this.walletManager.GetSpendableTransactionsInAccount(context.AccountReference, context.MinConfirmations).ToList(); context.UnspentOutputs = this.walletManager.GetSpendableTransactionsInAccount(context.AccountReference, context.MinConfirmations).ToList();
...@@ -334,7 +334,7 @@ namespace Stratis.Bitcoin.Features.Wallet ...@@ -334,7 +334,7 @@ namespace Stratis.Bitcoin.Features.Wallet
/// <remarks> /// <remarks>
/// Add outputs to the <see cref="TransactionBuilder"/> based on the <see cref="Recipient"/> list. /// Add outputs to the <see cref="TransactionBuilder"/> based on the <see cref="Recipient"/> list.
/// </remarks> /// </remarks>
private void AddRecipients(TransactionBuildContext context) protected void AddRecipients(TransactionBuildContext context)
{ {
if (context.Recipients.Any(a => a.Amount == Money.Zero)) if (context.Recipients.Any(a => a.Amount == Money.Zero))
throw new WalletException("No amount specified."); throw new WalletException("No amount specified.");
...@@ -373,7 +373,7 @@ namespace Stratis.Bitcoin.Features.Wallet ...@@ -373,7 +373,7 @@ namespace Stratis.Bitcoin.Features.Wallet
/// Add extra unspendable output to the transaction if there is anything in OpReturnData. /// Add extra unspendable output to the transaction if there is anything in OpReturnData.
/// </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>
private void AddOpReturnOutput(TransactionBuildContext context) protected void AddOpReturnOutput(TransactionBuildContext context)
{ {
if (string.IsNullOrEmpty(context.OpReturnData)) return; if (string.IsNullOrEmpty(context.OpReturnData)) return;
......
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