diff --git a/src/Angor.Test/ProjectIdentifierDerivationTests.cs b/src/Angor.Test/ProjectIdentifierDerivationTests.cs
index 68a3d326..a4a2506f 100644
--- a/src/Angor.Test/ProjectIdentifierDerivationTests.cs
+++ b/src/Angor.Test/ProjectIdentifierDerivationTests.cs
@@ -1,7 +1,5 @@
-using System;
using NBitcoin;
using NBitcoin.Secp256k1;
-using Xunit;
namespace Angor.Shared.Tests
{
@@ -19,8 +17,8 @@ public void TestComputeSharedSecretMethods()
var ecPubKey2 = ecPrivKey2.CreatePubKey();
// Act
- var sharedSecretFounder = ProjectIdentifierDerivation.ComputeSharedSecretFounder(ecPrivKey1, ecPubKey2);
- var sharedSecretAngor = ProjectIdentifierDerivation.ComputeSharedSecretAngor(ecPubKey1, ecPrivKey2);
+ var sharedSecretFounder = ProjectIdentifierDerivation.ComputeSharedSecretPublicKeySender(ecPrivKey1, ecPubKey2);
+ var sharedSecretAngor = ProjectIdentifierDerivation.ComputeSharedSecretPrivateKeyReceiver(ecPubKey1, ecPrivKey2);
// Assert
Assert.Equal(sharedSecretFounder.ToBytes(), sharedSecretAngor.CreatePubKey().ToBytes());
diff --git a/src/Angor/Shared/ByteHelpers.cs b/src/Angor/Shared/ByteHelpers.cs
deleted file mode 100644
index 7f3794d1..00000000
--- a/src/Angor/Shared/ByteHelpers.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-namespace Angor.Shared;
-
-public static unsafe class ByteHelpers
-{
- // Taken from wasabi wallet
- // https://stackoverflow.com/questions/415291/best-way-to-combine-two-or-more-byte-arrays-in-c-sharp
- ///
- /// Fastest byte array concatenation in C#
- ///
- public static byte[] Combine(params byte[][] arrays)
- {
- return Combine(arrays.AsEnumerable());
- }
-
- public static byte[] Combine(IEnumerable arrays)
- {
- byte[] ret = new byte[arrays.Sum(x => x.Length)];
- int offset = 0;
- foreach (byte[] data in arrays)
- {
- Buffer.BlockCopy(data, 0, ret, offset, data.Length);
- offset += data.Length;
- }
- return ret;
- }
-
- // https://stackoverflow.com/a/8808245/2061103
- // Copyright (c) 2008-2013 Hafthor Stefansson
- // Distributed under the MIT/X11 software license
- // Ref: http://www.opensource.org/licenses/mit-license.php.
- ///
- /// Fastest byte array comparison in C#
- ///
- public static unsafe bool CompareFastUnsafe(byte[]? array1, byte[]? array2)
- {
- if (array1 == array2)
- {
- return true;
- }
-
- if (array1 is null || array2 is null || array1.Length != array2.Length)
- {
- return false;
- }
-
- fixed (byte* p1 = array1, p2 = array2)
- {
- byte* x1 = p1, x2 = p2;
- int l = array1.Length;
- for (int i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
- {
- if (*((long*)x1) != *((long*)x2))
- {
- return false;
- }
- }
-
- if ((l & 4) != 0)
- {
- if (*((int*)x1) != *((int*)x2))
- {
- return false;
- }
- x1 += 4;
- x2 += 4;
- }
- if ((l & 2) != 0)
- {
- if (*((short*)x1) != *((short*)x2))
- {
- return false;
- }
- x1 += 2;
- x2 += 2;
- }
- if ((l & 1) != 0)
- {
- if (*x1 != *x2)
- {
- return false;
- }
- }
-
- return true;
- }
- }
-
- ///
- public static string ToHex(params byte[] bytes)
- {
- return Convert.ToHexString(bytes);
- }
-
- ///
- public static byte[] FromHex(string hex)
- {
- if (string.IsNullOrWhiteSpace(hex))
- {
- return Array.Empty();
- }
-
- return Convert.FromHexString(hex);
- }
-}
\ No newline at end of file
diff --git a/src/Angor/Shared/ProjectIdentifierDerivation.cs b/src/Angor/Shared/ProjectIdentifierDerivation.cs
index ce42756a..baa0012d 100644
--- a/src/Angor/Shared/ProjectIdentifierDerivation.cs
+++ b/src/Angor/Shared/ProjectIdentifierDerivation.cs
@@ -1,18 +1,17 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using NBitcoin;
using NBitcoin.Crypto;
-using NBitcoin.DataEncoders;
using NBitcoin.Secp256k1;
-using static NBitcoin.Scripting.OutputDescriptor;
-using static NBitcoin.Scripting.PubKeyProvider;
namespace Angor.Shared;
-public class ProjectIdentifierDerivation
+///
+/// This class is used to derive a shared secret between two parties.
+/// Using the shared secret, the parties can derive a new public key that is unique to the two parties.
+/// But only the receiver can derive the private key.
+/// Based on this issue https://github.com/block-core/angor/issues/172#issuecomment-2513031390
+///
+public class ProjectIdentifierDerivation
{
- public static ECPubKey ComputeSharedSecretFounder(ECPrivKey a, ECPubKey B)
+ public static ECPubKey ComputeSharedSecretPublicKeySender(ECPrivKey a, ECPubKey B)
{
// Let P = B + hash(a·B)·G
@@ -24,310 +23,14 @@ public static ECPubKey ComputeSharedSecretFounder(ECPrivKey a, ECPubKey B)
return publicKeyIdentifier;
}
- public static ECPrivKey ComputeSharedSecretAngor(ECPubKey A, ECPrivKey b)
+ public static ECPrivKey ComputeSharedSecretPrivateKeyReceiver(ECPubKey A, ECPrivKey b)
{
// Let p = b + hash(A·b)·G
ECPubKey sharedSecret = new ECPubKey(A.GetSharedPubkey(b).Q, null);
ECPrivKey hashedSharedSecret = ECPrivKey.Create(Hashes.SHA256(sharedSecret.ToBytes()));
- ECPrivKey privateKeyIdentifier = ECPrivKey.Create((hashedSharedSecret.sec + b.sec).ToBytes());
+ ECPrivKey privateKeyIdentifier = ECPrivKey.Create((hashedSharedSecret.sec + b.sec).ToBytes());
return privateKeyIdentifier;
}
-
- private static Scalar InputHash(ECPrivKey b, ECPubKey A)
- {
- var hash = Hashes.SHA256(A.TweakMul(b.sec.ToBytes()).ToBytes());
- return new Scalar(hash);
- }
-
- public static readonly byte[] NUMS =
- Encoders.Hex.DecodeData("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0");
-
- public static ECPubKey ComputeSharedSecretSender(Utxo[] utxos, ECPubKey B)
- {
- using var a = SumPrivateKeys(utxos);
- return ComputeSharedSecret(utxos.Select(x => x.OutPoint).ToArray(), a, B);
- }
-
- public static ECPubKey ComputeSharedSecretReceiver((OutPoint PrevOut, GE? PubKey)[] inputs, ECPrivKey b)
- {
- var A = SumPublicKeys(inputs.Where(x => x.PubKey is not null).Select(x => (GE)x.PubKey!));
- return ComputeSharedSecret(inputs.Select(x => x.PrevOut).ToArray(), A, b);
- }
-
- public static ECPubKey ComputeSharedSecretReceiver(ECPubKey tweakData, ECPrivKey b) =>
- new(tweakData.GetSharedPubkey(b).Q, null);
-
- private static ECPubKey ComputeSharedSecret(OutPoint[] outpoints, ECPrivKey a, ECPubKey B) =>
- DHSharedSecret(InputHash(outpoints, a.CreatePubKey()), B, a);
-
- private static ECPubKey ComputeSharedSecret(OutPoint[] outpoints, ECPubKey A, ECPrivKey b) =>
- DHSharedSecret(InputHash(outpoints, A), A, b);
-
- private static ECPubKey DHSharedSecret(Scalar inputHash, ECPubKey pubKey, ECPrivKey privKey) =>
- new(TweakData(inputHash, pubKey).GetSharedPubkey(privKey).Q, null);
-
- public static ECPubKey TweakData(OutPoint[] inputs, GE?[] As) =>
- TweakData(inputs, SumPublicKeys(As.Where(x => x is not null).Select(x => (GE)x!)));
-
- public static ECPubKey TweakData(OutPoint[] inputs, ECPubKey A) =>
- TweakData(InputHash(inputs, A), A);
-
- public static ECPubKey TweakData(Scalar inputHash, ECPubKey pubKey)
- {
- var ret = new ECPubKey((inputHash * pubKey.Q).ToGroupElement(), null);
- return ret;
- }
-
- // let tk = hash_BIP0352/SharedSecret(serP(ecdh_shared_secret) || ser32(k))
- private static ECPrivKey TweakKey(ECPubKey sharedSecret, uint k) =>
- ECPrivKey.Create(
- TaggedHash(
- "BIP0352/SharedSecret",
- ByteHelpers.Combine(sharedSecret.ToBytes(), Serialize32(k))));
-
- // let input_hash = hash_BIP0352/Inputs(outpointL || A)
- private static Scalar InputHash(OutPoint[] outpoints, ECPubKey A)
- {
- var outpointL = outpoints.Select(x => x.ToBytes()).Order(BytesComparer.Instance).First();
- var hash = TaggedHash("BIP0352/Inputs", ByteHelpers.Combine(outpointL, A.ToBytes()));
- return new Scalar(hash);
- }
-
- public static Dictionary GetPubKeys(IEnumerable recipients, Utxo[] utxos)
- {
- return recipients
- .GroupBy(x => x.ScanKey, (scanKey, addresses) =>
- {
- var sharedSecret = ComputeSharedSecretSender(utxos, scanKey);
- return addresses.Select((addr, k) => ComputePubKey(addr, (uint)k, sharedSecret));
- })
- .SelectMany(x => x)
- .GroupBy(x => x.Address)
- .ToDictionary(x => x.Key, x => x.ToArray());
- }
-
- public static IEnumerable GetPubKeys(SilentPaymentAddress[] addresses, ECPubKey sharedSecret, ECXOnlyPubKey[] outputs)
- {
- var found = 0;
- var n = 0;
- while (found == n)
- {
- var pns = addresses.Select(address => ComputePubKey(address, (uint)n, sharedSecret)).ToArray();
- if (outputs.FirstOrDefault(o => pns.Select(x => x.PubKey.Q).Contains(o.Q)) is { } nonNullOutput)
- {
- yield return nonNullOutput;
- found++;
- }
- else
- {
- foreach (var output in outputs)
- {
- if (pns.Select(pn => pn.PubKey.Q).Contains(output.Q))
- {
- yield return output;
- found++;
- }
- }
- }
-
- n++;
- }
- }
-
- public static bool IsElegible(Transaction tx)
- {
- var hasTaprootOutputs = tx.Outputs.Any(x => x.ScriptPubKey.IsScriptType(ScriptType.Taproot));
- return hasTaprootOutputs;
- }
-
- public static Script[] ExtractSilentPaymentScriptPubKeys(SilentPaymentAddress[] addresses, ECPubKey tweakData, Transaction tx, ECPrivKey spendKey)
- {
- if (IsElegible(tx))
- {
- var taprootPubKeys = tx.Outputs
- .Where(x => x.ScriptPubKey.IsScriptType(ScriptType.Taproot))
- .Select(x => PayToTaprootTemplate.Instance.ExtractScriptPubKeyParameters(x.ScriptPubKey))
- .Select(x => ECXOnlyPubKey.Create(x.ToBytes()))
- .ToArray();
- var sharedSecret = ComputeSharedSecretReceiver(tweakData, spendKey);
- var silentPaymentOutputs = GetPubKeys(addresses, sharedSecret, taprootPubKeys);
- return silentPaymentOutputs.Select(x => new TaprootPubKey(x.ToBytes()).ScriptPubKey).ToArray();
- }
-
- return new Script[0];
- }
-
- public static ECPubKey CreateLabel(ECPrivKey scanKey, uint label)
- {
- using var m = ECPrivKey.Create(
- TaggedHash(
- "BIP0352/Label",
- ByteHelpers.Combine(scanKey.sec.ToBytes(), Serialize32(label))));
- return m.CreatePubKey();
- }
-
- private static SilentPaymentPubKey ComputePubKey(SilentPaymentAddress addr, uint k, ECPubKey sharedSecret)
- {
- var pubkey = ComputePubKey(addr.SpendKey, sharedSecret, k).ToXOnlyPubKey();
- return new SilentPaymentPubKey(pubkey, addr);
- }
-
- public static Script ComputeScriptPubKey(ECPubKey spendKey, ECPubKey sharedSecret, uint k)
- {
- var pubkey = ComputePubKey(spendKey, sharedSecret, k);
- return new TaprootPubKey(pubkey.ToXOnlyPubKey().ToBytes()).ScriptPubKey;
- }
-
- public static ECPrivKey ComputePrivKey(ECPrivKey spendKey, ECPubKey sharedSecret, uint k)
- {
- using var tk = TweakKey(sharedSecret, k);
- return ECPrivKey.Create((tk.sec + spendKey.sec).ToBytes());
- }
-
- public static GE? ExtractPubKey(Script? scriptSig, WitScript? txInWitness, Script prevOutScriptPubKey)
- {
- var spk = prevOutScriptPubKey;
- if (txInWitness is { } && spk.IsScriptType(ScriptType.Taproot))
- {
- var pubKeyParameters = PayToTaprootTemplate.Instance.ExtractScriptPubKeyParameters(spk);
- var annex = txInWitness[txInWitness.PushCount - 1][^1] == 0x50 ? 1 : 0;
- if (txInWitness.PushCount > annex &&
- ByteHelpers.CompareFastUnsafe(txInWitness[txInWitness.PushCount - annex - 1][1..33], NUMS))
- {
- return null;
- }
- return ECXOnlyPubKey.Create(pubKeyParameters.ToBytes()).Q;
- }
- if (txInWitness is { } && spk.IsScriptType(ScriptType.P2WPKH))
- {
- var witScriptParameters =
- PayToWitPubKeyHashTemplate.Instance.ExtractWitScriptParameters(txInWitness);
- if (witScriptParameters is { } nonNullWitScriptParameters && nonNullWitScriptParameters.PublicKey.IsCompressed)
- {
- var q = ECPubKey.Create(nonNullWitScriptParameters.PublicKey.ToBytes()).ToXOnlyPubKey().Q;
- return nonNullWitScriptParameters.PublicKey.ToBytes()[0] == 0x02 ? q : q.Negate();
- }
- }
- if (scriptSig is { } && spk.IsScriptType(ScriptType.P2PKH))
- {
- var pk = scriptSig.GetAllPubKeys().First();
- return pk.IsCompressed && pk.GetScriptPubKey(ScriptPubKeyType.Legacy) == spk
- ? ECPubKey.Create(pk.ToBytes()).Q
- : null;
- }
- if (scriptSig is { } && spk.IsScriptType(ScriptType.P2SH))
- {
- var p2sh = PayToScriptHashTemplate.Instance.ExtractScriptSigParameters(scriptSig);
- if (txInWitness is { } && p2sh.RedeemScript.IsScriptType(ScriptType.P2WPKH))
- {
- var witScriptParameters =
- PayToWitPubKeyHashTemplate.Instance.ExtractWitScriptParameters(txInWitness);
- if (witScriptParameters is { } nonNullWitScriptParameters && nonNullWitScriptParameters.PublicKey.IsCompressed)
- {
- var q = ECPubKey.Create(nonNullWitScriptParameters.PublicKey.ToBytes()).ToXOnlyPubKey().Q;
- return nonNullWitScriptParameters.PublicKey.ToBytes()[0] == 0x02 ? q : q.Negate();
- }
- }
- }
-
- return null;
- }
-
- private static ECPubKey ComputePubKey(ECPubKey spendKey, ECPubKey sharedSecret, uint k)
- {
- using var tk = TweakKey(sharedSecret, k);
-
- // Let Pmk = k·G + Bm
- var pmk = tk.CreatePubKey().Q.ToGroupElementJacobian() + spendKey.Q;
- return new ECPubKey(pmk.ToGroupElement(), null);
- }
-
- private static ECPrivKey SumPrivateKeys(IEnumerable utxos)
- {
- var sum = utxos
- .Select(x => NegateKey(x.SigningKey, x.ScriptPubKey.IsScriptType(ScriptType.Taproot)))
- .Aggregate(Scalar.Zero, (acc, key) => acc.Add(key.sec));
-
- return ECPrivKey.Create(sum.ToBytes());
-
- ECPrivKey NegateKey(Key key, bool isTaproot)
- {
- var pk = ECPrivKey.Create(key.ToBytes());
- pk.CreateXOnlyPubKey(out var parity);
- return isTaproot && parity ? ECPrivKey.Create(pk.sec.Negate().ToBytes()) : pk;
- }
- }
-
- private static ECPubKey SumPublicKeys(IEnumerable pubKeys) =>
- new(pubKeys.Aggregate(GEJ.Infinity, (acc, key) => acc + key).ToGroupElement(), null);
-
- private static byte[] TaggedHash(string tag, byte[] data)
- {
- var tagHash = Hashes.SHA256(Encoding.UTF8.GetBytes(tag));
- var concat = ByteHelpers.Combine(tagHash, tagHash, data);
- return Hashes.SHA256(concat);
- }
-
- private static byte[] Serialize32(uint i)
- {
- var result = new byte[4];
- BitConverter.GetBytes(i).CopyTo(result, 0);
- if (BitConverter.IsLittleEndian)
- {
- Array.Reverse(result);
- }
- return result;
- }
-
-
-
- public record Utxo(OutPoint OutPoint, Key SigningKey, Script ScriptPubKey);
-
- public record SilentPaymentPubKey(ECXOnlyPubKey PubKey, SilentPaymentAddress Address);
-}
-
-public record SilentPaymentAddress(int Version, ECPubKey ScanKey, ECPubKey SpendKey)
-{
- //public static SilentPaymentAddress Parse(string encoded, Network network)
- //{
- // var spEncoder = network.GetSilentPaymentBech32Encoder();
- // var result = spEncoder.DecodeDataRaw(encoded, out _);
- // var version = result[0];
- // if (version != 0)
- // {
- // throw new FormatException("Unexpected version of silent payment code");
- // }
-
- // if (result.Length != 107)
- // {
- // throw new FormatException("Wrong lenght");
- // }
-
- // var data = spEncoder.FromBase32(result[1..]);
- // return new SilentPaymentAddress(
- // Version: 0,
- // ScanKey: ECPubKey.Create(data[..33]),
- // SpendKey: ECPubKey.Create(data[33..]));
- //}
-
- //public string ToWip(Network network)
- //{
- // var spEncoder = network.GetSilentPaymentBech32Encoder();
- // var data = new byte[66];
- // Buffer.BlockCopy(ScanKey.ToBytes(), 0, data, 0, 33);
- // Buffer.BlockCopy(SpendKey.ToBytes(), 0, data, 33, 33);
- // var base32 = spEncoder.ToBase32(data);
- // var buffer = new byte[base32.Length + 1];
- // buffer[0] = (byte)Version;
- // Buffer.BlockCopy(base32, 0, buffer, 1, base32.Length);
- // return spEncoder.EncodeRaw(buffer, Bech32EncodingType.BECH32M);
- //}
-
- public SilentPaymentAddress DeriveAddressForLabel(ECPubKey mG)
- {
- var bm = (SpendKey.Q.ToGroupElementJacobian() + mG.Q).ToGroupElement();
- return this with { SpendKey = new ECPubKey(bm, null) };
- }
}