btcpayserver/BTCPayServer/DerivationSchemeParser.cs

236 lines
9.0 KiB
C#
Raw Normal View History

2018-03-24 12:40:26 +01:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer
{
public class DerivationSchemeParser
{
2019-05-09 11:11:39 +02:00
public BTCPayNetwork BtcPayNetwork { get; }
2018-03-24 12:40:26 +01:00
2019-05-09 09:05:18 +02:00
public Network Network => BtcPayNetwork.NBitcoinNetwork;
public Script HintScriptPubKey { get; set; }
public DerivationSchemeParser(BTCPayNetwork expectedNetwork)
2018-03-24 12:40:26 +01:00
{
if (expectedNetwork == null)
throw new ArgumentNullException(nameof(expectedNetwork));
2019-05-09 09:05:18 +02:00
BtcPayNetwork = expectedNetwork;
2018-03-24 12:40:26 +01:00
}
2019-05-08 17:40:30 +02:00
public DerivationStrategyBase ParseElectrum(string str)
{
2019-05-09 09:05:18 +02:00
2019-05-08 17:40:30 +02:00
if (str == null)
throw new ArgumentNullException(nameof(str));
str = str.Trim();
var data = Network.GetBase58CheckEncoder().DecodeData(str);
if (data.Length < 4)
throw new FormatException();
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
2020-06-25 10:51:27 +02:00
var extPubKey = GetBitcoinExtPubKeyByNetwork(Network, data);
if (!BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
2019-05-08 17:40:30 +02:00
{
throw new FormatException();
}
if (type == DerivationType.Segwit)
2020-01-14 15:10:58 +01:00
return new DirectDerivationStrategy(extPubKey, true);
if (type == DerivationType.Legacy)
2020-01-14 15:10:58 +01:00
return new DirectDerivationStrategy(extPubKey, false);
if (type == DerivationType.SegwitP2SH)
2019-12-24 08:20:44 +01:00
return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(extPubKey.ToString() + "-[p2sh]");
2019-05-08 17:40:30 +02:00
throw new FormatException();
}
2018-03-24 12:40:26 +01:00
public DerivationStrategyBase Parse(string str)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
str = str.Trim();
HashSet<string> hintedLabels = new HashSet<string>();
var hintDestination = HintScriptPubKey?.GetDestination();
if (hintDestination != null)
{
if (hintDestination is KeyId)
{
hintedLabels.Add("legacy");
}
if (hintDestination is ScriptId)
{
hintedLabels.Add("p2sh");
}
}
2019-05-08 17:40:30 +02:00
if (!Network.Consensus.SupportSegwit)
2020-01-14 16:00:36 +01:00
{
2018-04-10 12:07:57 +02:00
hintedLabels.Add("legacy");
2020-01-14 16:00:36 +01:00
str = str.Replace("-[p2sh]", string.Empty, StringComparison.OrdinalIgnoreCase);
}
2018-04-10 12:07:57 +02:00
2018-03-24 12:40:26 +01:00
try
{
2019-12-24 08:20:44 +01:00
var result = BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str);
2018-03-24 12:40:26 +01:00
return FindMatch(hintedLabels, result);
}
catch
{
}
var parts = str.Split('-');
bool hasLabel = false;
2018-03-24 12:40:26 +01:00
for (int i = 0; i < parts.Length; i++)
{
if (IsLabel(parts[i]))
{
if (!hasLabel)
{
hintedLabels.Clear();
if (!Network.Consensus.SupportSegwit)
hintedLabels.Add("legacy");
}
hasLabel = true;
2018-03-24 12:40:26 +01:00
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
continue;
}
try
{
2018-06-23 17:45:57 +02:00
var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]);
2018-03-24 12:40:26 +01:00
if (data.Length < 4)
continue;
var prefix = Utils.ToUInt32(data, false);
2019-05-08 17:40:30 +02:00
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
2018-03-24 12:40:26 +01:00
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
2020-06-25 10:51:27 +02:00
var derivationScheme = GetBitcoinExtPubKeyByNetwork(Network, data).ToString();
2018-03-24 12:40:26 +01:00
2019-05-09 12:14:01 +02:00
if (BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
{
2019-05-09 12:14:01 +02:00
switch (type)
2018-03-24 12:40:26 +01:00
{
2019-05-09 12:14:01 +02:00
case DerivationType.Legacy:
hintedLabels.Add("legacy");
break;
case DerivationType.SegwitP2SH:
hintedLabels.Add("p2sh");
break;
2018-03-24 12:40:26 +01:00
}
}
parts[i] = derivationScheme;
}
catch { continue; }
}
if (hintDestination != null)
{
if (hintDestination is WitKeyId)
{
hintedLabels.Remove("legacy");
hintedLabels.Remove("p2sh");
}
}
str = string.Join('-', parts.Where(p => !IsLabel(p)));
foreach (var label in hintedLabels)
{
str = $"{str}-[{label}]";
}
2019-12-24 08:20:44 +01:00
return FindMatch(hintedLabels, BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str));
2018-03-24 12:40:26 +01:00
}
2020-06-25 10:51:27 +02:00
public static BitcoinExtPubKey GetBitcoinExtPubKeyByNetwork(Network network, byte[] data)
{
try
{
return new BitcoinExtPubKey(network.GetBase58CheckEncoder().EncodeData(data), network.NetworkSet.Mainnet).ToNetwork(network);
}
2020-06-26 13:52:39 +02:00
catch (Exception)
2020-06-25 10:51:27 +02:00
{
return new BitcoinExtPubKey(network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(network);
}
}
2018-03-24 12:40:26 +01:00
private DerivationStrategyBase FindMatch(HashSet<string> hintLabels, DerivationStrategyBase result)
{
var firstKeyPath = new KeyPath("0/0");
if (HintScriptPubKey == null)
return result;
2019-08-17 08:14:31 +02:00
if (HintScriptPubKey == result.GetDerivation(firstKeyPath).ScriptPubKey)
2018-03-24 12:40:26 +01:00
return result;
if (result is MultisigDerivationStrategy)
hintLabels.Add("keeporder");
var resultNoLabels = result.ToString();
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
foreach (var labels in ItemCombinations(hintLabels.ToList()))
{
2019-12-24 08:20:44 +01:00
var hinted = BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l => $"[{l}]").ToArray()));
2019-08-17 08:14:31 +02:00
if (HintScriptPubKey == hinted.GetDerivation(firstKeyPath).ScriptPubKey)
2018-03-24 12:40:26 +01:00
return hinted;
}
throw new FormatException("Could not find any match");
}
private static bool IsLabel(string v)
{
return v.StartsWith('[') && v.EndsWith(']');
}
/// <summary>
2019-05-08 17:40:30 +02:00
/// Method to create lists containing possible combinations of an input list of items. This is
/// basically copied from code by user "jaolho" on this thread:
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
/// </summary>
/// <typeparam name="T">type of the items on the input list</typeparam>
/// <param name="inputList">list of items</param>
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
/// if zero the empty combination is included,
/// default is one</param>
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
/// default is no maximum limit</param>
/// <returns>list of lists for possible combinations of the input items</returns>
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
int maximumItems = int.MaxValue)
2018-03-24 12:40:26 +01:00
{
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);
if (minimumItems == 0) // Optimize default case
listOfLists.Add(new List<T>());
for (int i = 1; i <= nonEmptyCombinations; i++)
{
List<T> thisCombination = new List<T>(inputList.Count);
for (int j = 0; j < inputList.Count; j++)
{
if ((i >> j & 1) == 1)
thisCombination.Add(inputList[j]);
}
if (thisCombination.Count >= minimumItems && thisCombination.Count <= maximumItems)
listOfLists.Add(thisCombination);
}
return listOfLists;
}
}
}