Skip to content

Commit d0b7525

Browse files
committed
Implement Musig PSBT (BIP373)
1 parent 4c5b2ac commit d0b7525

9 files changed

Lines changed: 291 additions & 8 deletions

File tree

NBitcoin/BIP174/PSBTInput.cs

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
using System.Diagnostics.CodeAnalysis;
1010
using NBitcoin.Crypto;
1111
using System.Text;
12+
using Newtonsoft.Json.Linq;
1213

1314
namespace NBitcoin
1415
{
15-
public class PSBTInput : PSBTCoin
16+
public partial class PSBTInput : PSBTCoin
1617
{
1718
// Those fields are not saved, but can be used as hint to solve more info for the PSBT
1819
internal Script originalScriptSig = Script.Empty;
@@ -159,6 +160,32 @@ internal PSBTInput(BitcoinStream stream, PSBT parent, uint index, TxIn input) :
159160
throw new FormatException("Invalid PSBTInput. Unexpected value length for PSBT_IN_TAP_MERKLE_ROOT");
160161
TaprootMerkleRoot = new uint256(v);
161162
break;
163+
#if HAS_SPAN
164+
case PSBTConstants.PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS:
165+
if (k.Length != 34)
166+
throw new FormatException("Invalid PSBTInput. Unexpected key length for PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS");
167+
if (v.Length % 33 != 0 || v.Length == 0)
168+
throw new FormatException("Invalid PSBTInput. Unexpected value length for PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS");
169+
var pk = NBitcoin.MusigParticipantPubKeys.Parse(k, v);
170+
this.MusigParticipantPubKeys.Add(pk.Aggregated, pk.PubKeys);
171+
break;
172+
case PSBTConstants.PSBT_IN_MUSIG2_PUB_NONCE:
173+
if (k.Length is not (1 + 33 + 33 or 1 + 33 + 33 + 32))
174+
throw new FormatException("Invalid PSBTInput. Unexpected key length for PSBT_IN_MUSIG2_PUB_NONCE");
175+
if (k.Length is not 66)
176+
throw new FormatException("Invalid PSBTInput. Unexpected value length for PSBT_IN_MUSIG2_PUB_NONCE");
177+
var musigNonceKey = MusigTarget.Parse(k);
178+
this.MusigPubNonces.Add(musigNonceKey, v);
179+
break;
180+
case PSBTConstants.PSBT_IN_MUSIG2_PARTIAL_SIG:
181+
if (k.Length is not (1 + 33 + 33 or 1 + 33 + 33 + 32))
182+
throw new FormatException("Invalid PSBTInput. Unexpected key length for PSBT_IN_MUSIG2_PARTIAL_SIG");
183+
if (k.Length is not 32)
184+
throw new FormatException("Invalid PSBTInput. Unexpected value length for PSBT_IN_MUSIG2_PARTIAL_SIG");
185+
var musigSigKey = MusigTarget.Parse(k);
186+
this.MusigPartialSigs.Add(musigSigKey, v);
187+
break;
188+
#endif
162189
case PSBTConstants.PSBT_IN_SCRIPTSIG:
163190
if (k.Length != 1)
164191
throw new FormatException("Invalid PSBTInput. Contains illegal value in key for final scriptsig");
@@ -416,7 +443,14 @@ public void UpdateFrom(PSBTInput other)
416443

417444
foreach (var keyPath in other.HDTaprootKeyPaths)
418445
HDTaprootKeyPaths.TryAdd(keyPath.Key, keyPath.Value);
419-
446+
#if HAS_SPAN
447+
foreach (var o in other.MusigParticipantPubKeys)
448+
MusigParticipantPubKeys.TryAdd(o.Key, o.Value);
449+
foreach (var o in other.MusigPartialSigs)
450+
MusigPartialSigs.TryAdd(o.Key, o.Value);
451+
foreach (var o in other.MusigPubNonces)
452+
MusigPubNonces.TryAdd(o.Key, o.Value);
453+
#endif
420454
TaprootInternalKey ??= other.TaprootInternalKey;
421455
TaprootKeySignature ??= other.TaprootKeySignature;
422456
TaprootMerkleRoot ??= other.TaprootMerkleRoot;
@@ -728,7 +762,32 @@ public void Serialize(BitcoinStream stream)
728762
var value = merkleRoot.ToBytes();
729763
stream.ReadWriteAsVarString(ref value);
730764
}
731-
765+
#if HAS_SPAN
766+
foreach (var mpk in MusigParticipantPubKeys)
767+
{
768+
var key = new byte[] { PSBTConstants.PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS }.Concat(mpk.Key.ToBytes());
769+
stream.ReadWriteAsVarString(ref key);
770+
foreach (var pk in mpk.Value)
771+
{
772+
var b = pk.ToBytes();
773+
stream.ReadWriteAsVarString(ref b);
774+
}
775+
}
776+
foreach (var mpn in MusigPubNonces)
777+
{
778+
var key = mpn.Key.ToBytes(PSBTConstants.PSBT_IN_MUSIG2_PUB_NONCE);
779+
stream.ReadWriteAsVarString(ref key);
780+
var v = mpn.Value;
781+
stream.ReadWriteAsVarString(ref v);
782+
}
783+
foreach (var mpn in MusigPartialSigs)
784+
{
785+
var key = mpn.Key.ToBytes(PSBTConstants.PSBT_IN_MUSIG2_PARTIAL_SIG);
786+
stream.ReadWriteAsVarString(ref key);
787+
var v = mpn.Value;
788+
stream.ReadWriteAsVarString(ref v);
789+
}
790+
#endif
732791
if (this.TaprootInternalKey is TaprootInternalPubKey tp)
733792
{
734793
stream.ReadWriteAsVarInt(ref defaultKeyLen);
@@ -856,13 +915,53 @@ internal void Write(JsonTextWriter jsonWriter)
856915
{
857916
jsonWriter.WritePropertyValue("taproot_key_signature", tsig.ToString());
858917
}
918+
859919
jsonWriter.WritePropertyName("partial_signatures");
860920
jsonWriter.WriteStartObject();
861921
foreach (var sig in partial_sigs)
862922
{
863923
jsonWriter.WritePropertyValue(sig.Key.ToString(), Encoders.Hex.EncodeData(sig.Value.ToBytes()));
864924
}
865925
jsonWriter.WriteEndObject();
926+
#if HAS_SPAN
927+
if (MusigParticipantPubKeys.Count != 0)
928+
{
929+
jsonWriter.WritePropertyName("musig_participant_pubkeys");
930+
jsonWriter.WriteStartObject();
931+
foreach (var o in MusigParticipantPubKeys)
932+
{
933+
jsonWriter.WritePropertyName(o.Key.ToHex());
934+
jsonWriter.WriteStartArray();
935+
foreach (var k in o.Value)
936+
{
937+
jsonWriter.WriteValue(k.ToHex());
938+
}
939+
jsonWriter.WriteEndArray();
940+
}
941+
jsonWriter.WriteEndObject();
942+
}
943+
944+
if (MusigPartialSigs.Count != 0)
945+
{
946+
jsonWriter.WritePropertyName("musig_partial_signatures");
947+
jsonWriter.WriteStartObject();
948+
foreach (var sig in MusigPartialSigs)
949+
{
950+
jsonWriter.WritePropertyValue(Encoders.Hex.EncodeData(sig.Key.ToBytes(0)[1..]), Encoders.Hex.EncodeData(sig.Value));
951+
}
952+
jsonWriter.WriteEndObject();
953+
}
954+
if (MusigPartialSigs.Count != 0)
955+
{
956+
jsonWriter.WritePropertyName("musig_pub_nonces");
957+
jsonWriter.WriteStartObject();
958+
foreach (var sig in MusigPubNonces)
959+
{
960+
jsonWriter.WritePropertyValue(Encoders.Hex.EncodeData(sig.Key.ToBytes(0)[1..]), Encoders.Hex.EncodeData(sig.Value));
961+
}
962+
jsonWriter.WriteEndObject();
963+
}
964+
#endif
866965
if (sighash_type is uint s)
867966
jsonWriter.WritePropertyValue("sighash", GetName(s));
868967
if (this.FinalScriptSig != null)

NBitcoin/BIP174/PSBTOutput.cs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
namespace NBitcoin
1717
{
18-
public class PSBTOutput : PSBTCoin
18+
public partial class PSBTOutput : PSBTCoin
1919
{
2020
internal TxOut TxOut { get; }
2121
public Script ScriptPubKey => TxOut.ScriptPubKey;
@@ -108,9 +108,19 @@ internal PSBTOutput(BitcoinStream stream, PSBT parent, uint index, TxOut txOut)
108108
throw new FormatException("Invalid PSBTOutput. Contains invalid internal taproot pubkey");
109109
TaprootInternalKey = tpk;
110110
break;
111+
#if HAS_SPAN
112+
case PSBTConstants.PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS:
113+
if (k.Length != 34)
114+
throw new FormatException("Invalid PSBTOutput. Unexpected key length for PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS");
115+
if (v.Length % 33 != 0 || v.Length == 0)
116+
throw new FormatException("Invalid PSBTOutput. Unexpected value length for PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS");
117+
var pk = NBitcoin.MusigParticipantPubKeys.Parse(k, v);
118+
this.MusigParticipantPubKeys.Add(pk.Aggregated, pk.PubKeys);
119+
break;
120+
#endif
111121
default:
112122
if (unknown.ContainsKey(k))
113-
throw new FormatException("Invalid PSBTInput, duplicate key for unknown value");
123+
throw new FormatException("Invalid PSBTOutput, duplicate key for unknown value");
114124
unknown.Add(k, v);
115125
break;
116126
}
@@ -137,6 +147,11 @@ public void UpdateFrom(PSBTOutput other)
137147

138148
foreach (var uk in other.Unknown)
139149
unknown.TryAdd(uk.Key, uk.Value);
150+
151+
#if HAS_SPAN
152+
foreach (var o in other.MusigParticipantPubKeys)
153+
MusigParticipantPubKeys.TryAdd(o.Key, o.Value);
154+
#endif
140155
}
141156

142157
#region IBitcoinSerializable Members
@@ -194,7 +209,18 @@ public void Serialize(BitcoinStream stream)
194209
b = ((MemoryStream)bs.Inner).ToArrayEfficient();
195210
stream.ReadWriteAsVarString(ref b);
196211
}
197-
212+
#if HAS_SPAN
213+
foreach (var mpk in MusigParticipantPubKeys)
214+
{
215+
var key = new byte[] { PSBTConstants.PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS }.Concat(mpk.Key.ToBytes());
216+
stream.ReadWriteAsVarString(ref key);
217+
foreach (var pk in mpk.Value)
218+
{
219+
var b = pk.ToBytes();
220+
stream.ReadWriteAsVarString(ref b);
221+
}
222+
}
223+
#endif
198224
foreach (var entry in unknown)
199225
{
200226
var k = entry.Key;
@@ -277,6 +303,24 @@ internal void Write(JsonTextWriter jsonWriter)
277303
{
278304
jsonWriter.WritePropertyValue("witness_script", witness_script.ToString());
279305
}
306+
#if HAS_SPAN
307+
if (MusigParticipantPubKeys.Count != 0)
308+
{
309+
jsonWriter.WritePropertyName("musig_participant_pubkeys");
310+
jsonWriter.WriteStartObject();
311+
foreach (var o in MusigParticipantPubKeys)
312+
{
313+
jsonWriter.WritePropertyName(o.Key.ToHex());
314+
jsonWriter.WriteStartArray();
315+
foreach (var k in o.Value)
316+
{
317+
jsonWriter.WriteValue(k.ToHex());
318+
}
319+
jsonWriter.WriteEndArray();
320+
}
321+
jsonWriter.WriteEndObject();
322+
}
323+
#endif
280324
jsonWriter.WriteBIP32Derivations(this.hd_keypaths);
281325
jsonWriter.WriteBIP32Derivations(this.hd_taprootkeypaths);
282326
jsonWriter.WriteEndObject();

NBitcoin/BIP174/PartiallySignedTransaction.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ static PSBTConstants()
5858
public const byte PSBT_IN_TAP_BIP32_DERIVATION = 0x16;
5959
public const byte PSBT_OUT_TAP_BIP32_DERIVATION = 0x07;
6060
public const byte PSBT_IN_TAP_MERKLE_ROOT = 0x18;
61+
public const byte PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1a;
62+
public const byte PSBT_IN_MUSIG2_PUB_NONCE = 0x1b;
63+
public const byte PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1c;
64+
public const byte PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = 0x08;
6165

6266
// Output types
6367
public const byte PSBT_OUT_REDEEMSCRIPT = 0x00;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#if HAS_SPAN
2+
#nullable enable
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace NBitcoin
10+
{
11+
internal class MusigParticipantPubKeys
12+
{
13+
public MusigParticipantPubKeys(PubKey aggregated, PubKey[] pubKeys)
14+
{
15+
if (aggregated is null)
16+
throw new ArgumentNullException(nameof(aggregated));
17+
if (pubKeys is null)
18+
throw new ArgumentNullException(nameof(pubKeys));
19+
if (pubKeys.Length is 0)
20+
throw new ArgumentException("pubKeys cannot be an empty collection.", nameof(pubKeys));
21+
if (aggregated.IsCompressed)
22+
throw new ArgumentException("The aggregated key must be uncompressed.", nameof(aggregated));
23+
foreach (var pk in pubKeys)
24+
{
25+
if (pk is null)
26+
throw new ArgumentNullException(nameof(pubKeys), "pubKeys cannot contain null elements.");
27+
if (!pk.IsCompressed)
28+
throw new ArgumentException("All public keys must be compressed.", nameof(pubKeys));
29+
}
30+
Aggregated = aggregated;
31+
PubKeys = pubKeys;
32+
}
33+
/// <summary>
34+
/// 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.
35+
/// </summary>
36+
public PubKey Aggregated { get; private set; }
37+
38+
/// <summary>
39+
/// 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.
40+
/// </summary>
41+
public PubKey[] PubKeys { get; private set; }
42+
43+
internal static MusigParticipantPubKeys Parse(ReadOnlySpan<byte> key, ReadOnlySpan<byte> value)
44+
{
45+
var agg = new PubKey(key[1..]);
46+
var pubKeys = new PubKey[value.Length / 33];
47+
int index = 0;
48+
for (int i = 0; i < value.Length; i += 33)
49+
{
50+
pubKeys[index++] = new PubKey(value.Slice(i, 33));
51+
}
52+
return new MusigParticipantPubKeys(agg, pubKeys);
53+
}
54+
}
55+
}
56+
#endif

NBitcoin/BIP373/MusigTarget.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#if HAS_SPAN
2+
#nullable enable
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace NBitcoin
10+
{
11+
public record MusigTarget(PubKey ParticipantPubKey, PubKey AggregatePubKey, uint256? TapLeaf) : IComparable<MusigTarget>
12+
{
13+
internal static MusigTarget Parse(ReadOnlySpan<byte> k)
14+
{
15+
var participant = new PubKey(k[1..]);
16+
if (!participant.IsCompressed)
17+
throw new FormatException("The participant public key must be compressed.");
18+
var agg = new PubKey(k[(1 + 33)..]);
19+
if (!participant.IsCompressed)
20+
throw new FormatException("The aggregate public key must be compressed.");
21+
var tapleaf = k[(1 + 33 + 33)..];
22+
var h = tapleaf.Length is 0 ? null : new uint256(tapleaf);
23+
return new MusigTarget(participant, agg, h);
24+
}
25+
26+
public int CompareTo(MusigTarget? other) => other is null ? 1 : PubKeyComparer.Instance.Compare(ParticipantPubKey, other?.ParticipantPubKey);
27+
28+
public byte[] ToBytes(byte key)
29+
{
30+
var result = new byte[1 + 33 + 33 + (TapLeaf is null ? 0 : 32)];
31+
result[0] = key;
32+
ParticipantPubKey.ToBytes(result.AsSpan(1), out _);
33+
ParticipantPubKey.ToBytes(result.AsSpan(1 + 33), out _);
34+
if (TapLeaf is not null)
35+
TapLeaf.ToBytes(result.AsSpan(1 + 33 + 33));
36+
return result;
37+
}
38+
}
39+
}
40+
#endif

NBitcoin/BIP373/PSBTInput.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#if HAS_SPAN
2+
#nullable enable
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace NBitcoin
10+
{
11+
public partial class PSBTInput
12+
{
13+
public SortedDictionary<PubKey, PubKey[]> MusigParticipantPubKeys { get; } = new SortedDictionary<PubKey, PubKey[]>(PubKeyComparer.Instance);
14+
public SortedDictionary<MusigTarget, byte[]> MusigPubNonces { get; } = new SortedDictionary<MusigTarget, byte[]>();
15+
public SortedDictionary<MusigTarget, byte[]> MusigPartialSigs { get; } = new SortedDictionary<MusigTarget, byte[]>();
16+
}
17+
}
18+
#endif

NBitcoin/BIP373/PSBTOutput.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#if HAS_SPAN
2+
#nullable enable
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace NBitcoin
10+
{
11+
public partial class PSBTOutput
12+
{
13+
public SortedDictionary<PubKey, PubKey[]> MusigParticipantPubKeys { get; } = new SortedDictionary<PubKey, PubKey[]>(PubKeyComparer.Instance);
14+
}
15+
}
16+
#endif

NBitcoin/LegacyShims.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#if LEGACY_SHIMS
2+
namespace System.Runtime.CompilerServices
3+
{
4+
internal static class IsExternalInit { }
5+
}
6+
#endif

0 commit comments

Comments
 (0)