Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions NBitcoin.Tests/PSBTTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
using System.Linq;
using static NBitcoin.Tests.Comparer;
using Xunit.Abstractions;
using System.Net.Http;
using System.Threading.Tasks;

namespace NBitcoin.Tests
{
Expand Down Expand Up @@ -41,6 +39,27 @@ public static void ShouldThrowExceptionForInvalidData()
}
}

[Fact]
[Trait("UnitTest", "UnitTest")]
public static void CanDeriveHDKey()
{
var k = new ExtKey();
var kwif = k.GetWif(Network.Main);
var pk = k.Neuter();
var pkwif = pk.GetWif(Network.Main);
static void AssertEqualKey(IHDKey a, IHDKey b)
{
Assert.Equal(a.GetPublicKey(), b.GetPublicKey());
}
AssertEqualKey(((IHDKey)k).Derive(new KeyPath("1")), ((IHDKey)pk).Derive(new KeyPath("1")));
AssertEqualKey(((IHDKey)kwif).Derive(new KeyPath("1")), ((IHDKey)pkwif).Derive(new KeyPath("1")));
AssertEqualKey(((IHDKey)k).Derive(new KeyPath("1")), ((IHDKey)kwif).Derive(new KeyPath("1")));
Assert.Null(((IHDKey)pk).Derive(new KeyPath("1'")));
Assert.Null(((IHDKey)pkwif).Derive(new KeyPath("1'")));
Assert.NotNull(((IHDKey)k).Derive(new KeyPath("1'")));
Assert.NotNull(((IHDKey)kwif).Derive(new KeyPath("1'")));
}

[Theory]
[InlineData(PSBTVersion.PSBTv0)]
[InlineData(PSBTVersion.PSBTv2)]
Expand All @@ -52,7 +71,7 @@ public static void ShouldCalculateBalanceOfHDKey(PSBTVersion version)
var aliceMaster = new ExtKey();
var bobMaster = new ExtKey();

var alice = aliceMaster.Derive(new KeyPath("1/2/3"));
var alice = aliceMaster.Derive(new KeyPath("1'/2/3"));
var bob = bobMaster.Derive(new KeyPath("4/5/6"));

var funding = network.CreateTransaction();
Expand Down Expand Up @@ -83,18 +102,21 @@ public static void ShouldCalculateBalanceOfHDKey(PSBTVersion version)
builder.SendFees(Money.Coins(0.001m));

var psbt = builder.BuildPSBT(false, version);
psbt.AddKeyPath(aliceMaster, new KeyPath("1/2/3"));
psbt.AddKeyPath(aliceMaster, new KeyPath("1'/2/3"));
psbt.AddKeyPath(bobMaster, new KeyPath("4/5/6"));

var actualBalance = psbt.GetBalance(ScriptPubKeyType.Legacy, aliceMaster);
var expectedChange = aliceCoin.Amount - (Money.Coins(0.2m) + Money.Coins(0.1m) + Money.Coins(0.123m));
var expectedBalance = -aliceCoin.Amount + expectedChange;
Assert.Equal(expectedBalance, actualBalance);
// We can't derive Alice's balance from the xpub only
Assert.Equal(Money.Zero, psbt.GetBalance(ScriptPubKeyType.Legacy, aliceMaster.Neuter()));

actualBalance = psbt.GetBalance(ScriptPubKeyType.Legacy, bobMaster);
expectedChange = bobCoin.Amount - (Money.Coins(0.25m) + Money.Coins(0.01m) + Money.Coins(0.001m)) + Money.Coins(0.123m);
expectedBalance = -bobCoin.Amount + expectedChange;
Assert.Equal(expectedBalance, actualBalance);
Assert.Equal(expectedBalance, psbt.GetBalance(ScriptPubKeyType.Legacy, bobMaster.Neuter()));

Assert.False(psbt.TryGetFee(out _));
Assert.False(psbt.IsReadyToSign());
Expand Down
39 changes: 18 additions & 21 deletions NBitcoin/BIP174/HDKeyCache.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;
Expand All @@ -9,7 +10,7 @@ class HDKeyCache : IHDKey
{
private readonly IHDKey hdKey;
private readonly KeyPath _PathFromRoot;
private readonly ConcurrentDictionary<KeyPath, IHDKey> derivationCache;
private readonly ConcurrentDictionary<KeyPath, IHDKey?> derivationCache;
public IHDKey Inner
{
get
Expand All @@ -21,16 +22,16 @@ internal HDKeyCache(IHDKey masterKey)
{
this.hdKey = masterKey;
_PathFromRoot = new KeyPath();
derivationCache = new ConcurrentDictionary<KeyPath, IHDKey>();
derivationCache = new ConcurrentDictionary<KeyPath, IHDKey?>();
}
HDKeyCache(IHDKey hdKey, KeyPath childPath, ConcurrentDictionary<KeyPath, IHDKey> cache)
HDKeyCache(IHDKey hdKey, KeyPath childPath, ConcurrentDictionary<KeyPath, IHDKey?> cache)
{
this.derivationCache = cache;
_PathFromRoot = childPath;
this.hdKey = hdKey;
}

public IHDKey Derive(KeyPath keyPath)
public IHDKey? Derive(KeyPath keyPath)
{
if (keyPath == null)
throw new ArgumentNullException(nameof(keyPath));
Expand All @@ -39,7 +40,11 @@ public IHDKey Derive(KeyPath keyPath)
foreach (var index in keyPath.Indexes)
{
childPath = childPath.Derive(index);
key = derivationCache.GetOrAdd(childPath, _ => key.Derive(new KeyPath(index)));
if (childPath is null)
return null;
key = derivationCache.GetOrAdd(childPath, _ => key?.Derive(new KeyPath(index)));
if (key is null)
return null;
}
return new HDKeyCache(key, childPath, derivationCache);
}
Expand All @@ -50,17 +55,12 @@ public PubKey GetPublicKey()
{
return this.hdKey.GetPublicKey();
}

public bool CanDeriveHardenedPath()
{
return Inner.CanDeriveHardenedPath();
}
}
class HDScriptPubKeyCache : IHDScriptPubKey
{
private readonly IHDScriptPubKey hdKey;
private readonly KeyPath _PathFromRoot;
private readonly ConcurrentDictionary<KeyPath, IHDScriptPubKey> derivationCache;
private readonly ConcurrentDictionary<KeyPath, IHDScriptPubKey?> derivationCache;
public IHDScriptPubKey Inner
{
get
Expand All @@ -72,16 +72,16 @@ internal HDScriptPubKeyCache(IHDScriptPubKey masterKey)
{
this.hdKey = masterKey;
_PathFromRoot = new KeyPath();
derivationCache = new ConcurrentDictionary<KeyPath, IHDScriptPubKey>();
derivationCache = new ConcurrentDictionary<KeyPath, IHDScriptPubKey?>();
}
HDScriptPubKeyCache(IHDScriptPubKey hdKey, KeyPath childPath, ConcurrentDictionary<KeyPath, IHDScriptPubKey> cache)
HDScriptPubKeyCache(IHDScriptPubKey hdKey, KeyPath childPath, ConcurrentDictionary<KeyPath, IHDScriptPubKey?> cache)
{
this.derivationCache = cache;
_PathFromRoot = childPath;
this.hdKey = hdKey;
}

public IHDScriptPubKey Derive(KeyPath keyPath)
public IHDScriptPubKey? Derive(KeyPath keyPath)
{
if (keyPath == null)
throw new ArgumentNullException(nameof(keyPath));
Expand All @@ -90,18 +90,15 @@ public IHDScriptPubKey Derive(KeyPath keyPath)
foreach (var index in keyPath.Indexes)
{
childPath = childPath.Derive(index);
key = derivationCache.GetOrAdd(childPath, _ => key.Derive(new KeyPath(index)));
key = derivationCache.GetOrAdd(childPath, _ => key?.Derive(new KeyPath(index)));
if (key is null)
return null;
}
return new HDScriptPubKeyCache(key, childPath, derivationCache);
}

internal int Cached => derivationCache.Count;

public Script ScriptPubKey => Inner.ScriptPubKey;

public bool CanDeriveHardenedPath()
{
return Inner.CanDeriveHardenedPath();
}
}
}
29 changes: 14 additions & 15 deletions NBitcoin/BIP174/PSBTCoin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,17 @@ bool Match(KeyValuePair<IPubKey, RootedKeyPath> hdKey, HDFingerprint? expectedMa
if (expectedMasterFp is not null &&
hdKey.Value.MasterFingerprint != expectedMasterFp.Value)
return false;
if (accountHDScriptPubKey is not null &&
accountHDScriptPubKey.Derive(addressPath).ScriptPubKey != coinScriptPubKey)
return false;
if (accountHDScriptPubKey is not null)
{
var derivedScript = accountHDScriptPubKey.Derive(addressPath);
if (derivedScript is null)
return false;
if (derivedScript.ScriptPubKey != coinScriptPubKey)
return false;
}
var derived = accountKey.Derive(addressPath);
if (derived is null)
return false;
return hdKey.Key switch
{
PubKey pk => derived.GetPublicKey().Equals(pk),
Expand All @@ -142,20 +149,14 @@ bool Match(KeyValuePair<IPubKey, RootedKeyPath> hdKey, HDFingerprint? expectedMa
accountHDScriptPubKey = accountHDScriptPubKey?.AsHDKeyCache();
foreach (var hdKey in EnumerateKeyPaths())
{
bool matched = false;
var canDeriveHardenedPath = (accountKey.CanDeriveHardenedPath() && (accountHDScriptPubKey == null || accountHDScriptPubKey.CanDeriveHardenedPath()));
// The case where the fingerprint of the hdkey is exactly equal to the accountKey
if (!hdKey.Value.KeyPath.IsHardenedPath || canDeriveHardenedPath)
if (Match(hdKey, accountFingerprint, accountKey, hdKey.Value.KeyPath))
{
if (Match(hdKey, accountFingerprint, accountKey, hdKey.Value.KeyPath))
{
yield return CreateHDKeyMatch(accountKey, hdKey.Value.KeyPath, hdKey);
matched = true;
}
yield return CreateHDKeyMatch(accountKey, hdKey.Value.KeyPath, hdKey);
}

// The typical case where accountkey is based on an hardened derivation (eg. 49'/0'/0')
if (!matched && accountKeyPath?.MasterFingerprint is HDFingerprint mp)
else if (accountKeyPath?.MasterFingerprint is HDFingerprint mp)
{
var addressPath = hdKey.Value.KeyPath.GetAddressKeyPath();
// The cases where addresses are generated on a non-hardened path below it (eg. 49'/0'/0'/0/1)
Expand All @@ -164,12 +165,11 @@ bool Match(KeyValuePair<IPubKey, RootedKeyPath> hdKey, HDFingerprint? expectedMa
if (Match(hdKey, mp, accountKey, addressPath))
{
yield return CreateHDKeyMatch(accountKey, addressPath, hdKey);
matched = true;
}
}
// in some cases addresses are generated on a hardened path below the account key (eg. 49'/0'/0'/0'/1') in which case we
// need to brute force what the address key path is
else if (canDeriveHardenedPath) // We can only do this if we can derive hardened paths
else
{
int addressPathSize = 0;
var hdKeyIndexes = hdKey.Value.KeyPath.Indexes;
Expand All @@ -181,7 +181,6 @@ bool Match(KeyValuePair<IPubKey, RootedKeyPath> hdKey, HDFingerprint? expectedMa
if (Match(hdKey, null, accountKey, addressPath))
{
yield return CreateHDKeyMatch(accountKey, addressPath, hdKey);
matched = true;
break;
}
addressPathSize++;
Expand Down
5 changes: 4 additions & 1 deletion NBitcoin/BIP174/PSBTInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,10 @@ internal void TrySign(IHDScriptPubKey? accountHDScriptPubKey, IHDKey accountKey,
var cache = accountKey.AsHDKeyCache();
foreach (var hdk in this.HDKeysFor(accountHDScriptPubKey, cache, accountKeyPath))
{
if (((HDKeyCache)cache.Derive(hdk.AddressKeyPath)).Inner is ISecret k)
var key = cache.Derive(hdk.AddressKeyPath);
if (key is null)
continue;
if (((HDKeyCache)key).Inner is ISecret k)
Sign(k.PrivateKey, signingOptions);
else
throw new ArgumentException(paramName: nameof(accountKey), message: "This should be a private key");
Expand Down
2 changes: 2 additions & 0 deletions NBitcoin/BIP174/PartiallySignedTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,8 @@ public PSBT AddKeyPath(IHDKey masterKey, params Tuple<KeyPath, Script?>[] paths)
foreach (var path in paths)
{
var key = masterKey.Derive(path.Item1);
if (key is null)
continue;
AddKeyPath(key.GetPublicKey(), new RootedKeyPath(masterKeyFP, path.Item1), path.Item2);
}
return this;
Expand Down
53 changes: 14 additions & 39 deletions NBitcoin/BIP32/BitcoinExtKey.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;

namespace NBitcoin
{
Expand Down Expand Up @@ -60,7 +62,7 @@ protected override bool IsValid
}
}

ExtKey _Key;
ExtKey? _Key;

/// <summary>
/// Gets the extended key, converting from the Base58 representation.
Expand Down Expand Up @@ -113,10 +115,7 @@ public BitcoinExtKey Derive(uint index)
return new BitcoinExtKey(ExtKey.Derive(index), Network);
}

IHDKey IHDKey.Derive(KeyPath keyPath)
{
return Derive(keyPath);
}
IHDKey? IHDKey.Derive(KeyPath keyPath) => Derive(keyPath);

public BitcoinExtKey Derive(KeyPath keyPath)
{
Expand All @@ -138,11 +137,6 @@ public PubKey GetPublicKey()
return ExtKey.PrivateKey.PubKey;
}

bool IHDKey.CanDeriveHardenedPath()
{
return true;
}

#region ISecret Members

/// <summary>
Expand All @@ -158,15 +152,10 @@ public Key PrivateKey

#endregion

/// <summary>
/// Implicit cast from BitcoinExtKey to ExtKey.
/// </summary>
public static implicit operator ExtKey(BitcoinExtKey key)
{
if (key == null)
return null;
return key.ExtKey;
}

#nullable disable
public static implicit operator ExtKey(BitcoinExtKey key) => key?.ExtKey;
#nullable enable
}

/// <summary>
Expand All @@ -190,7 +179,7 @@ public BitcoinExtPubKey(ExtPubKey key, Network network)
{
}

ExtPubKey _PubKey;
ExtPubKey? _PubKey;

/// <summary>
/// Gets the extended public key, converting from the Base58 representation.
Expand Down Expand Up @@ -246,20 +235,11 @@ public override Script ScriptPubKey
}
}

/// <summary>
/// Implicit cast from BitcoinExtPubKey to ExtPubKey.
/// </summary>
public static implicit operator ExtPubKey(BitcoinExtPubKey key)
{
if (key == null)
return null;
return key.ExtPubKey;
}
#nullable disable
public static implicit operator ExtPubKey(BitcoinExtPubKey key) => key?.ExtPubKey;
#nullable enable

IHDKey IHDKey.Derive(KeyPath keyPath)
{
return Derive(keyPath);
}
IHDKey? IHDKey.Derive(KeyPath keyPath) => keyPath?.IsHardenedPath is true ? null : Derive(keyPath!);

public BitcoinExtPubKey Derive(uint index)
{
Expand All @@ -277,10 +257,5 @@ public PubKey GetPublicKey()
{
return ExtPubKey.pubkey;
}

bool IHDKey.CanDeriveHardenedPath()
{
return false;
}
}
}
Loading
Loading