Allow signing on non segwit transactions via the ledger

This commit is contained in:
nicolas.dorier
2018-05-07 12:17:46 +09:00
parent 75d685ae6c
commit 6ca8ba9231
2 changed files with 78 additions and 92 deletions

View File

@@ -10,6 +10,7 @@ using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Services; using BTCPayServer.Services;
using LedgerWallet;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NBitcoin; using NBitcoin;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
@@ -264,7 +265,7 @@ namespace BTCPayServer.Controllers
{ {
var strategy = GetDirectDerivationStrategy(store, network); var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network); var strategyBase = GetDerivationStrategy(store, network);
if (strategy == null || !await hw.SupportDerivation(network, strategy)) if (strategy == null || await hw.GetKeyPath(network, strategy) == null)
{ {
throw new Exception($"This store is not configured to use this ledger"); throw new Exception($"This store is not configured to use this ledger");
} }
@@ -286,11 +287,76 @@ namespace BTCPayServer.Controllers
var unspentCoins = await wallet.GetUnspentCoins(strategyBase); var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change; var changeAddress = await change;
var transaction = await hw.SendToAddress(strategy, unspentCoins, network, var send = new[] { (
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) }, destination: destinationAddress as IDestination,
feeRateValue, amount: amountBTC,
changeAddress.Item1, substractFees: subsctractFeesValue) };
changeAddress.Item2, summary.Status.BitcoinStatus.MinRelayTxFee);
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await hw.GetKeyPath(network, strategy);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
builder.SendEstimatedFees(feeRateValue);
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
if(!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _ExplorerProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach(var getTransactionAsync in getTransactionAsyncs)
{
var tx = (await getTransactionAsync.Op);
if(tx == null)
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
}
}
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
{
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null);
try try
{ {
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction }); var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
@@ -336,8 +402,6 @@ namespace BTCPayServer.Controllers
var directStrategy = strategy as DirectDerivationStrategy; var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null) if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy; directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
if (!directStrategy.Segwit)
return null;
return directStrategy; return directStrategy;
} }

View File

@@ -118,18 +118,7 @@ namespace BTCPayServer.Services
} }
} }
public async Task<bool> SupportDerivation(BTCPayNetwork network, DirectDerivationStrategy strategy) public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
if (!strategy.Segwit)
return false;
return await GetKeyPath(_Ledger, network, strategy) != null;
}
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
{ {
List<KeyPath> derivations = new List<KeyPath>(); List<KeyPath> derivations = new List<KeyPath>();
if(network.NBitcoinNetwork.Consensus.SupportSegwit) if(network.NBitcoinNetwork.Consensus.SupportSegwit)
@@ -143,7 +132,7 @@ namespace BTCPayServer.Services
{ {
try try
{ {
var extpubkey = await GetExtPubKey(ledger, network, account, true); var extpubkey = await GetExtPubKey(_Ledger, network, account, true);
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey) if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
{ {
foundKeyPath = account; foundKeyPath = account;
@@ -159,79 +148,12 @@ namespace BTCPayServer.Services
return foundKeyPath; return foundKeyPath;
} }
public async Task<Transaction> SendToAddress(DirectDerivationStrategy strategy, public async Task<Transaction> SignTransactionAsync(SignatureRequest[] signatureRequests,
ReceivedCoin[] coins, BTCPayNetwork network, Transaction unsigned,
(IDestination destination, Money amount, bool substractFees)[] send, KeyPath changeKeyPath)
FeeRate feeRate,
IDestination changeAddress,
KeyPath changeKeyPath,
FeeRate minTxRelayFee)
{ {
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
if (network == null)
throw new ArgumentNullException(nameof(network));
if (feeRate == null)
throw new ArgumentNullException(nameof(feeRate));
if (changeAddress == null)
throw new ArgumentNullException(nameof(changeAddress));
if (feeRate.FeePerK <= Money.Zero)
{
throw new ArgumentOutOfRangeException(nameof(feeRate), "The fee rate should be above zero");
}
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await GetKeyPath(Ledger, network, strategy);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress);
builder.SendEstimatedFees(feeRate);
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach(var c in coins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
_Transport.Timeout = TimeSpan.FromMinutes(5); _Transport.Timeout = TimeSpan.FromMinutes(5);
var fullySigned = await Ledger.SignTransactionAsync( return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath);
usedCoins.Select(c => new SignatureRequest
{
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(),
unsigned,
hasChange ? foundKeyPath.Derive(changeKeyPath) : null);
return fullySigned;
} }
} }