Skip to content

Commit

Permalink
Implement Musig PSBT (BIP373)
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasDorier committed Nov 8, 2024
1 parent 4c5b2ac commit d0b7525
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 8 deletions.
105 changes: 102 additions & 3 deletions NBitcoin/BIP174/PSBTInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
using System.Diagnostics.CodeAnalysis;
using NBitcoin.Crypto;
using System.Text;
using Newtonsoft.Json.Linq;

namespace NBitcoin
{
public class PSBTInput : PSBTCoin
public partial class PSBTInput : PSBTCoin
{
// Those fields are not saved, but can be used as hint to solve more info for the PSBT
internal Script originalScriptSig = Script.Empty;
Expand Down Expand Up @@ -159,6 +160,32 @@ internal PSBTInput(BitcoinStream stream, PSBT parent, uint index, TxIn input) :
throw new FormatException("Invalid PSBTInput. Unexpected value length for PSBT_IN_TAP_MERKLE_ROOT");
TaprootMerkleRoot = new uint256(v);
break;
#if HAS_SPAN
case PSBTConstants.PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS:
if (k.Length != 34)
throw new FormatException("Invalid PSBTInput. Unexpected key length for PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS");
if (v.Length % 33 != 0 || v.Length == 0)
throw new FormatException("Invalid PSBTInput. Unexpected value length for PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS");
var pk = NBitcoin.MusigParticipantPubKeys.Parse(k, v);
this.MusigParticipantPubKeys.Add(pk.Aggregated, pk.PubKeys);
break;
case PSBTConstants.PSBT_IN_MUSIG2_PUB_NONCE:
if (k.Length is not (1 + 33 + 33 or 1 + 33 + 33 + 32))
throw new FormatException("Invalid PSBTInput. Unexpected key length for PSBT_IN_MUSIG2_PUB_NONCE");
if (k.Length is not 66)
throw new FormatException("Invalid PSBTInput. Unexpected value length for PSBT_IN_MUSIG2_PUB_NONCE");
var musigNonceKey = MusigTarget.Parse(k);
this.MusigPubNonces.Add(musigNonceKey, v);
break;
case PSBTConstants.PSBT_IN_MUSIG2_PARTIAL_SIG:
if (k.Length is not (1 + 33 + 33 or 1 + 33 + 33 + 32))
throw new FormatException("Invalid PSBTInput. Unexpected key length for PSBT_IN_MUSIG2_PARTIAL_SIG");
if (k.Length is not 32)
throw new FormatException("Invalid PSBTInput. Unexpected value length for PSBT_IN_MUSIG2_PARTIAL_SIG");
var musigSigKey = MusigTarget.Parse(k);
this.MusigPartialSigs.Add(musigSigKey, v);
break;
#endif
case PSBTConstants.PSBT_IN_SCRIPTSIG:
if (k.Length != 1)
throw new FormatException("Invalid PSBTInput. Contains illegal value in key for final scriptsig");
Expand Down Expand Up @@ -416,7 +443,14 @@ public void UpdateFrom(PSBTInput other)

foreach (var keyPath in other.HDTaprootKeyPaths)
HDTaprootKeyPaths.TryAdd(keyPath.Key, keyPath.Value);

#if HAS_SPAN
foreach (var o in other.MusigParticipantPubKeys)
MusigParticipantPubKeys.TryAdd(o.Key, o.Value);
foreach (var o in other.MusigPartialSigs)
MusigPartialSigs.TryAdd(o.Key, o.Value);
foreach (var o in other.MusigPubNonces)
MusigPubNonces.TryAdd(o.Key, o.Value);
#endif
TaprootInternalKey ??= other.TaprootInternalKey;
TaprootKeySignature ??= other.TaprootKeySignature;
TaprootMerkleRoot ??= other.TaprootMerkleRoot;
Expand Down Expand Up @@ -728,7 +762,32 @@ public void Serialize(BitcoinStream stream)
var value = merkleRoot.ToBytes();
stream.ReadWriteAsVarString(ref value);
}

#if HAS_SPAN
foreach (var mpk in MusigParticipantPubKeys)
{
var key = new byte[] { PSBTConstants.PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS }.Concat(mpk.Key.ToBytes());
stream.ReadWriteAsVarString(ref key);
foreach (var pk in mpk.Value)
{
var b = pk.ToBytes();
stream.ReadWriteAsVarString(ref b);
}
}
foreach (var mpn in MusigPubNonces)
{
var key = mpn.Key.ToBytes(PSBTConstants.PSBT_IN_MUSIG2_PUB_NONCE);
stream.ReadWriteAsVarString(ref key);
var v = mpn.Value;
stream.ReadWriteAsVarString(ref v);
}
foreach (var mpn in MusigPartialSigs)
{
var key = mpn.Key.ToBytes(PSBTConstants.PSBT_IN_MUSIG2_PARTIAL_SIG);
stream.ReadWriteAsVarString(ref key);
var v = mpn.Value;
stream.ReadWriteAsVarString(ref v);
}
#endif
if (this.TaprootInternalKey is TaprootInternalPubKey tp)
{
stream.ReadWriteAsVarInt(ref defaultKeyLen);
Expand Down Expand Up @@ -856,13 +915,53 @@ internal void Write(JsonTextWriter jsonWriter)
{
jsonWriter.WritePropertyValue("taproot_key_signature", tsig.ToString());
}

jsonWriter.WritePropertyName("partial_signatures");
jsonWriter.WriteStartObject();
foreach (var sig in partial_sigs)
{
jsonWriter.WritePropertyValue(sig.Key.ToString(), Encoders.Hex.EncodeData(sig.Value.ToBytes()));
}
jsonWriter.WriteEndObject();
#if HAS_SPAN
if (MusigParticipantPubKeys.Count != 0)
{
jsonWriter.WritePropertyName("musig_participant_pubkeys");
jsonWriter.WriteStartObject();
foreach (var o in MusigParticipantPubKeys)
{
jsonWriter.WritePropertyName(o.Key.ToHex());
jsonWriter.WriteStartArray();
foreach (var k in o.Value)
{
jsonWriter.WriteValue(k.ToHex());
}
jsonWriter.WriteEndArray();
}
jsonWriter.WriteEndObject();
}

if (MusigPartialSigs.Count != 0)
{
jsonWriter.WritePropertyName("musig_partial_signatures");
jsonWriter.WriteStartObject();
foreach (var sig in MusigPartialSigs)
{
jsonWriter.WritePropertyValue(Encoders.Hex.EncodeData(sig.Key.ToBytes(0)[1..]), Encoders.Hex.EncodeData(sig.Value));
}
jsonWriter.WriteEndObject();
}
if (MusigPartialSigs.Count != 0)
{
jsonWriter.WritePropertyName("musig_pub_nonces");
jsonWriter.WriteStartObject();
foreach (var sig in MusigPubNonces)
{
jsonWriter.WritePropertyValue(Encoders.Hex.EncodeData(sig.Key.ToBytes(0)[1..]), Encoders.Hex.EncodeData(sig.Value));
}
jsonWriter.WriteEndObject();
}
#endif
if (sighash_type is uint s)
jsonWriter.WritePropertyValue("sighash", GetName(s));
if (this.FinalScriptSig != null)
Expand Down
50 changes: 47 additions & 3 deletions NBitcoin/BIP174/PSBTOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

namespace NBitcoin
{
public class PSBTOutput : PSBTCoin
public partial class PSBTOutput : PSBTCoin
{
internal TxOut TxOut { get; }
public Script ScriptPubKey => TxOut.ScriptPubKey;
Expand Down Expand Up @@ -108,9 +108,19 @@ internal PSBTOutput(BitcoinStream stream, PSBT parent, uint index, TxOut txOut)
throw new FormatException("Invalid PSBTOutput. Contains invalid internal taproot pubkey");
TaprootInternalKey = tpk;
break;
#if HAS_SPAN
case PSBTConstants.PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS:
if (k.Length != 34)
throw new FormatException("Invalid PSBTOutput. Unexpected key length for PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS");
if (v.Length % 33 != 0 || v.Length == 0)
throw new FormatException("Invalid PSBTOutput. Unexpected value length for PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS");
var pk = NBitcoin.MusigParticipantPubKeys.Parse(k, v);
this.MusigParticipantPubKeys.Add(pk.Aggregated, pk.PubKeys);
break;
#endif
default:
if (unknown.ContainsKey(k))
throw new FormatException("Invalid PSBTInput, duplicate key for unknown value");
throw new FormatException("Invalid PSBTOutput, duplicate key for unknown value");
unknown.Add(k, v);
break;
}
Expand All @@ -137,6 +147,11 @@ public void UpdateFrom(PSBTOutput other)

foreach (var uk in other.Unknown)
unknown.TryAdd(uk.Key, uk.Value);

#if HAS_SPAN
foreach (var o in other.MusigParticipantPubKeys)
MusigParticipantPubKeys.TryAdd(o.Key, o.Value);
#endif
}

#region IBitcoinSerializable Members
Expand Down Expand Up @@ -194,7 +209,18 @@ public void Serialize(BitcoinStream stream)
b = ((MemoryStream)bs.Inner).ToArrayEfficient();
stream.ReadWriteAsVarString(ref b);
}

#if HAS_SPAN
foreach (var mpk in MusigParticipantPubKeys)
{
var key = new byte[] { PSBTConstants.PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS }.Concat(mpk.Key.ToBytes());
stream.ReadWriteAsVarString(ref key);
foreach (var pk in mpk.Value)
{
var b = pk.ToBytes();
stream.ReadWriteAsVarString(ref b);
}
}
#endif
foreach (var entry in unknown)
{
var k = entry.Key;
Expand Down Expand Up @@ -277,6 +303,24 @@ internal void Write(JsonTextWriter jsonWriter)
{
jsonWriter.WritePropertyValue("witness_script", witness_script.ToString());
}
#if HAS_SPAN
if (MusigParticipantPubKeys.Count != 0)
{
jsonWriter.WritePropertyName("musig_participant_pubkeys");
jsonWriter.WriteStartObject();
foreach (var o in MusigParticipantPubKeys)
{
jsonWriter.WritePropertyName(o.Key.ToHex());
jsonWriter.WriteStartArray();
foreach (var k in o.Value)
{
jsonWriter.WriteValue(k.ToHex());
}
jsonWriter.WriteEndArray();
}
jsonWriter.WriteEndObject();
}
#endif
jsonWriter.WriteBIP32Derivations(this.hd_keypaths);
jsonWriter.WriteBIP32Derivations(this.hd_taprootkeypaths);
jsonWriter.WriteEndObject();
Expand Down
4 changes: 4 additions & 0 deletions NBitcoin/BIP174/PartiallySignedTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ static PSBTConstants()
public const byte PSBT_IN_TAP_BIP32_DERIVATION = 0x16;
public const byte PSBT_OUT_TAP_BIP32_DERIVATION = 0x07;
public const byte PSBT_IN_TAP_MERKLE_ROOT = 0x18;
public const byte PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1a;
public const byte PSBT_IN_MUSIG2_PUB_NONCE = 0x1b;
public const byte PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1c;
public const byte PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = 0x08;

// Output types
public const byte PSBT_OUT_REDEEMSCRIPT = 0x00;
Expand Down
56 changes: 56 additions & 0 deletions NBitcoin/BIP373/MusigParticipantPubKeys.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#if HAS_SPAN
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NBitcoin
{
internal class MusigParticipantPubKeys
{
public MusigParticipantPubKeys(PubKey aggregated, PubKey[] pubKeys)
{
if (aggregated is null)
throw new ArgumentNullException(nameof(aggregated));
if (pubKeys is null)
throw new ArgumentNullException(nameof(pubKeys));
if (pubKeys.Length is 0)
throw new ArgumentException("pubKeys cannot be an empty collection.", nameof(pubKeys));
if (aggregated.IsCompressed)
throw new ArgumentException("The aggregated key must be uncompressed.", nameof(aggregated));
foreach (var pk in pubKeys)
{
if (pk is null)
throw new ArgumentNullException(nameof(pubKeys), "pubKeys cannot contain null elements.");
if (!pk.IsCompressed)
throw new ArgumentException("All public keys must be compressed.", nameof(pubKeys));
}
Aggregated = aggregated;
PubKeys = pubKeys;
}
/// <summary>
/// The MuSig2 aggregate plain public key[1] from the KeyAgg algorithm. This key may or may not be in the script directly (as x-only). It may instead be a parent public key from which the public keys in the script were derived.
/// </summary>
public PubKey Aggregated { get; private set; }

/// <summary>
/// A list of the compressed public keys of the participants in the MuSig2 aggregate key in the order required for aggregation. If sorting was done, then the keys must be in the sorted order.
/// </summary>
public PubKey[] PubKeys { get; private set; }

internal static MusigParticipantPubKeys Parse(ReadOnlySpan<byte> key, ReadOnlySpan<byte> value)
{
var agg = new PubKey(key[1..]);
var pubKeys = new PubKey[value.Length / 33];
int index = 0;
for (int i = 0; i < value.Length; i += 33)
{
pubKeys[index++] = new PubKey(value.Slice(i, 33));
}
return new MusigParticipantPubKeys(agg, pubKeys);
}
}
}
#endif
40 changes: 40 additions & 0 deletions NBitcoin/BIP373/MusigTarget.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#if HAS_SPAN
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NBitcoin
{
public record MusigTarget(PubKey ParticipantPubKey, PubKey AggregatePubKey, uint256? TapLeaf) : IComparable<MusigTarget>
{
internal static MusigTarget Parse(ReadOnlySpan<byte> k)
{
var participant = new PubKey(k[1..]);
if (!participant.IsCompressed)
throw new FormatException("The participant public key must be compressed.");
var agg = new PubKey(k[(1 + 33)..]);
if (!participant.IsCompressed)
throw new FormatException("The aggregate public key must be compressed.");
var tapleaf = k[(1 + 33 + 33)..];
var h = tapleaf.Length is 0 ? null : new uint256(tapleaf);
return new MusigTarget(participant, agg, h);
}

public int CompareTo(MusigTarget? other) => other is null ? 1 : PubKeyComparer.Instance.Compare(ParticipantPubKey, other?.ParticipantPubKey);

public byte[] ToBytes(byte key)
{
var result = new byte[1 + 33 + 33 + (TapLeaf is null ? 0 : 32)];
result[0] = key;
ParticipantPubKey.ToBytes(result.AsSpan(1), out _);
ParticipantPubKey.ToBytes(result.AsSpan(1 + 33), out _);
if (TapLeaf is not null)
TapLeaf.ToBytes(result.AsSpan(1 + 33 + 33));
return result;
}
}
}
#endif
18 changes: 18 additions & 0 deletions NBitcoin/BIP373/PSBTInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#if HAS_SPAN
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NBitcoin
{
public partial class PSBTInput
{
public SortedDictionary<PubKey, PubKey[]> MusigParticipantPubKeys { get; } = new SortedDictionary<PubKey, PubKey[]>(PubKeyComparer.Instance);
public SortedDictionary<MusigTarget, byte[]> MusigPubNonces { get; } = new SortedDictionary<MusigTarget, byte[]>();
public SortedDictionary<MusigTarget, byte[]> MusigPartialSigs { get; } = new SortedDictionary<MusigTarget, byte[]>();
}
}
#endif
16 changes: 16 additions & 0 deletions NBitcoin/BIP373/PSBTOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#if HAS_SPAN
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NBitcoin
{
public partial class PSBTOutput
{
public SortedDictionary<PubKey, PubKey[]> MusigParticipantPubKeys { get; } = new SortedDictionary<PubKey, PubKey[]>(PubKeyComparer.Instance);
}
}
#endif
6 changes: 6 additions & 0 deletions NBitcoin/LegacyShims.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#if LEGACY_SHIMS
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit { }
}
#endif
Loading

0 comments on commit d0b7525

Please sign in to comment.