Skip to content

Commit

Permalink
Hook up fee estimation to transaction creation.
Browse files Browse the repository at this point in the history
This change requires a newer version of the wallet's RPC API.

Fixes btcsuite#10.
Fixes btcsuite#11.
  • Loading branch information
jrick committed Jul 5, 2016
1 parent 5ab43d7 commit 96ae9bc
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 219 deletions.
4 changes: 2 additions & 2 deletions Paymetheus.Decred/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,8 @@ public void SerializeTo(byte[] destination, int offset = 0)
}
}

private const int RedeemPayToPubKeyHashSigScriptSize = 1 + 73 + 1 + 33;
private const int PayToPubKeyHashPkScriptSize = 1 + 1 + 1 + 20 + 1 + 1;
public const int RedeemPayToPubKeyHashSigScriptSize = 1 + 73 + 1 + 33;
public const int PayToPubKeyHashPkScriptSize = 1 + 1 + 1 + 20 + 1 + 1;

// Worst case sizes for compressed p2pkh inputs and outputs.
// Used for estimating an unsigned transaction's worst case serialize size
Expand Down
22 changes: 13 additions & 9 deletions Paymetheus.Decred/TransactionRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Copyright (c) 2016 The Decred developers
// Licensed under the ISC license. See LICENSE file in the project root for full license information.

using Paymetheus.Decred.Util;
using System;
using System.Collections.Generic;

Expand All @@ -27,23 +28,26 @@ public static class TransactionRules
public static bool IsSaneOutputValue(Amount a) => a >= 0 && a <= MaxOutputValue;

/// <summary>
/// Check whether an output is considered dust for a given transaction relay fee.
/// Check whether an output amount is considered dust for a given transaction relay fee.
/// Transactions with dust outputs are rejected by mempool.
/// </summary>
/// <param name="output">Transaction output to check</param>
/// <param name="amount">Output amount</param>
/// <param name="scriptSize">Size of the output script in bytes</param>
/// <param name="relayFeePerKb">Mempool relay fee/kB</param>
/// <returns>Whether the output is dust</returns>
public static bool IsDust(Transaction.Output output, Amount relayFeePerKb)
public static bool IsDustAmount(Amount amount, int scriptSize, Amount relayFeePerKb)
{
// TODO: Rather than assumming the output is P2PKH and using the size of a
// P2PKH input script to estimate the total cost to the network, a better
// estimate could be used if the output script is one of the other recognized
// script kinds.
var totalSize = output.SerializeSize + Transaction.RedeemPayToPubKeyHashInputSize;
// Calculate the total (estimated) cost to the network. This is
// calculated using the serialize size of the output plus the serial
// size of a transaction input which redeems it. The output is assumed
// to be compressed P2PKH as this is the most common script type. Use
// the average size of a compressed P2PKH redeem input (165) rather than
// the largest possible (Transaction.RedeemPayToPubKeyHashInputSize).
var totalSize = 8 + 2 + CompactInt.SerializeSize((ulong)scriptSize) + scriptSize + 165;

// Dust is defined as an output value where the total cost to the network
// (output size + input size) is greater than 1/3 of the relay fee.
return output.Amount * 1000 / (3 * totalSize) < relayFeePerKb;
return amount * 1000 / (3 * totalSize) < relayFeePerKb;
}

public static void CheckSanity(Transaction tx)
Expand Down
49 changes: 34 additions & 15 deletions Paymetheus.Decred/Wallet/TransactionAuthor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ public static class TransactionAuthor
/// <returns>Total previous output amount and array of transaction inputs if the target was met.</returns>
public delegate Task<Tuple<Amount, Transaction.Input[]>> InputSource(Amount target);

/// <summary>
/// Provides P2PKH change output scripts for transaction creation.
/// </summary>
/// <returns>An output script used to return all extra input value</returns>
public delegate Task<OutputScript> ChangeSource();

/// <summary>
/// Constructs an unsigned transaction by referencing previous unspent outputs.
/// A change output is added when necessary to return extra value back to the wallet.
Expand All @@ -29,16 +35,16 @@ public static class TransactionAuthor
/// <returns>Unsigned transaction and total input amount.</returns>
/// <exception cref="InsufficientFundsException">Input source was unable to provide enough input value.</exception>
public static async Task<Tuple<Transaction, Amount>> BuildUnsignedTransaction(Transaction.Output[] outputs,
OutputScript changeScript,
Amount feePerKb,
InputSource fetchInputsAsync)
InputSource fetchInputsAsync,
ChangeSource fetchChangeAsync)
{
if (outputs == null)
throw new ArgumentNullException(nameof(outputs));
if (changeScript == null)
throw new ArgumentNullException(nameof(changeScript));
if (fetchInputsAsync == null)
throw new ArgumentNullException(nameof(fetchInputsAsync));
if (fetchChangeAsync == null)
throw new ArgumentNullException(nameof(fetchChangeAsync));

var targetAmount = outputs.Sum(o => o.Amount);
var estimatedSize = Transaction.EstimateSerializeSize(1, outputs, true);
Expand All @@ -54,22 +60,35 @@ public static async Task<Tuple<Transaction, Amount>> BuildUnsignedTransaction(Tr
throw new InsufficientFundsException();
}

var unsignedTransaction = new Transaction(Transaction.SupportedVersion, inputs, outputs, 0, 0);
if (inputAmount > targetAmount + targetFee)
var maxSignedSize = Transaction.EstimateSerializeSize(inputs.Length, outputs, true);
var maxRequiredFee = TransactionFees.FeeForSerializeSize(feePerKb, maxSignedSize);
var remainingAmount = inputAmount - maxRequiredFee;
if (remainingAmount < maxRequiredFee)
{
unsignedTransaction = TransactionFees.AddChange(unsignedTransaction, inputAmount,
changeScript, feePerKb);
targetFee = maxRequiredFee;
continue;
}

if (TransactionFees.EstimatedFeePerKb(unsignedTransaction, inputAmount) < feePerKb)
{
estimatedSize = Transaction.EstimateSerializeSize(inputs.Length, outputs, true);
targetFee = TransactionFees.FeeForSerializeSize(feePerKb, estimatedSize);
}
else
var unsignedTransaction = new Transaction(Transaction.SupportedVersion, inputs, outputs, 0, 0);
var changeAmount = inputAmount - targetAmount - targetFee;
if (changeAmount != 0 && !TransactionRules.IsDustAmount(changeAmount, Transaction.PayToPubKeyHashPkScriptSize, feePerKb))
{
return Tuple.Create(unsignedTransaction, inputAmount);
var changeScript = await fetchChangeAsync();
if (changeScript.Script.Length > Transaction.PayToPubKeyHashPkScriptSize)
{
throw new Exception("Fee estimation requires change scripts no larger than P2PKH output scripts");
}
var changeOutput = new Transaction.Output(changeAmount, Transaction.Output.LatestPkScriptVersion, changeScript.Script);

var outputList = unsignedTransaction.Outputs.ToList();
outputList.Add(changeOutput);
var outputsWithChange = outputList.ToArray();

unsignedTransaction = new Transaction(unsignedTransaction.Version, unsignedTransaction.Inputs, outputsWithChange,
unsignedTransaction.LockTime, unsignedTransaction.Expiry);
}

return Tuple.Create(unsignedTransaction, inputAmount);
}
}
}
Expand Down
36 changes: 1 addition & 35 deletions Paymetheus.Decred/Wallet/TransactionFees.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Paymetheus.Decred.Wallet
{
public static class TransactionFees
{
public static readonly Amount DefaultFeePerKb = 100000;
public static readonly Amount DefaultFeePerKb = 1000000;

public static Amount FeeForSerializeSize(Amount feePerKb, int txSerializeSize)
{
Expand Down Expand Up @@ -52,39 +52,5 @@ public static Amount EstimatedFeePerKb(Transaction tx, Amount totalInput)
var actualFee = ActualFee(tx, totalInput);
return actualFee * 1000 / estimatedSize;
}

/// <summary>
/// Potentially adds a change output to a transaction to set an appropiate fee.
/// </summary>
public static Transaction AddChange(Transaction tx, Amount totalInput, OutputScript changeScript, Amount feePerKb)
{
if (tx == null)
throw new ArgumentNullException(nameof(tx));
if (totalInput < 0)
throw Errors.RequireNonNegative(nameof(totalInput));
if (changeScript == null)
throw new ArgumentNullException(nameof(changeScript));
if (feePerKb < 0)
throw Errors.RequireNonNegative(nameof(feePerKb));

var txSerializeSizeEstimate = Transaction.EstimateSerializeSize(tx.Inputs.Length, tx.Outputs, true);
var feeEstimate = FeeForSerializeSize(feePerKb, txSerializeSizeEstimate);

var totalNonChangeOutput = tx.Outputs.Sum(o => o.Amount);
var changeAmount = totalInput - totalNonChangeOutput - feeEstimate;
var changeOutput = new Transaction.Output(changeAmount, Transaction.SupportedVersion, changeScript.Script);

// Change should not be created if the change output itself would be considered dust.
if (TransactionRules.IsDust(changeOutput, feePerKb))
{
return tx;
}

var outputList = tx.Outputs.ToList();
outputList.Add(changeOutput); // TODO: Randomize change output position.
var outputs = outputList.ToArray();

return new Transaction(tx.Version, tx.Inputs, outputs, tx.LockTime, tx.Expiry);
}
}
}
4 changes: 3 additions & 1 deletion Paymetheus.Decred/Wallet/UnspentOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Paymetheus.Decred.Wallet
{
public sealed class UnspentOutput
{
public UnspentOutput(Blake256Hash txHash, uint outputIndex, Amount amount, OutputScript pkScript, DateTimeOffset seenTime, bool isFromCoinbase)
public UnspentOutput(Blake256Hash txHash, uint outputIndex, byte tree, Amount amount, OutputScript pkScript, DateTimeOffset seenTime, bool isFromCoinbase)
{
if (txHash == null)
throw new ArgumentNullException(nameof(txHash));
Expand All @@ -18,6 +18,7 @@ public UnspentOutput(Blake256Hash txHash, uint outputIndex, Amount amount, Outpu

TransactionHash = txHash;
OutputIndex = outputIndex;
Tree = tree;
Amount = amount;
PkScript = pkScript;
SeenTime = seenTime;
Expand All @@ -26,6 +27,7 @@ public UnspentOutput(Blake256Hash txHash, uint outputIndex, Amount amount, Outpu

public Blake256Hash TransactionHash { get; }
public uint OutputIndex { get; }
public byte Tree { get; }
public Amount Amount { get; }
public OutputScript PkScript { get; }
public DateTimeOffset SeenTime { get; }
Expand Down
Loading

0 comments on commit 96ae9bc

Please sign in to comment.