Skip to content

Commit 0d853c4

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

File tree

9 files changed

+290
-8
lines changed

9 files changed

+290
-8
lines changed

NBitcoin/BIP174/PSBTInput.cs

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

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

417443
foreach (var keyPath in other.HDTaprootKeyPaths)
418444
HDTaprootKeyPaths.TryAdd(keyPath.Key, keyPath.Value);
419-
445+
#if HAS_SPAN
446+
foreach (var o in other.MusigParticipantPubKeys)
447+
MusigParticipantPubKeys.TryAdd(o.Key, o.Value);
448+
foreach (var o in other.MusigPartialSigs)
449+
MusigPartialSigs.TryAdd(o.Key, o.Value);
450+
foreach (var o in other.MusigPubNonces)
451+
MusigPubNonces.TryAdd(o.Key, o.Value);
452+
#endif
420453
TaprootInternalKey ??= other.TaprootInternalKey;
421454
TaprootKeySignature ??= other.TaprootKeySignature;
422455
TaprootMerkleRoot ??= other.TaprootMerkleRoot;
@@ -728,7 +761,32 @@ public void Serialize(BitcoinStream stream)
728761
var value = merkleRoot.ToBytes();
729762
stream.ReadWriteAsVarString(ref value);
730763
}
731-
764+
#if HAS_SPAN
765+
foreach (var mpk in MusigParticipantPubKeys)
766+
{
767+
var key = new byte[] { PSBTConstants.PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS }.Concat(mpk.Key.ToBytes());
768+
stream.ReadWriteAsVarString(ref key);
769+
foreach (var pk in mpk.Value)
770+
{
771+
var b = pk.ToBytes();
772+
stream.ReadWriteAsVarString(ref b);
773+
}
774+
}
775+
foreach (var mpn in MusigPubNonces)
776+
{
777+
var key = mpn.Key.ToBytes(PSBTConstants.PSBT_IN_MUSIG2_PUB_NONCE);
778+
stream.ReadWriteAsVarString(ref key);
779+
var v = mpn.Value;
780+
stream.ReadWriteAsVarString(ref v);
781+
}
782+
foreach (var mpn in MusigPartialSigs)
783+
{
784+
var key = mpn.Key.ToBytes(PSBTConstants.PSBT_IN_MUSIG2_PARTIAL_SIG);
785+
stream.ReadWriteAsVarString(ref key);
786+
var v = mpn.Value;
787+
stream.ReadWriteAsVarString(ref v);
788+
}
789+
#endif
732790
if (this.TaprootInternalKey is TaprootInternalPubKey tp)
733791
{
734792
stream.ReadWriteAsVarInt(ref defaultKeyLen);
@@ -856,13 +914,53 @@ internal void Write(JsonTextWriter jsonWriter)
856914
{
857915
jsonWriter.WritePropertyValue("taproot_key_signature", tsig.ToString());
858916
}
917+
859918
jsonWriter.WritePropertyName("partial_signatures");
860919
jsonWriter.WriteStartObject();
861920
foreach (var sig in partial_sigs)
862921
{
863922
jsonWriter.WritePropertyValue(sig.Key.ToString(), Encoders.Hex.EncodeData(sig.Value.ToBytes()));
864923
}
865924
jsonWriter.WriteEndObject();
925+
#if HAS_SPAN
926+
if (MusigParticipantPubKeys.Count != 0)
927+
{
928+
jsonWriter.WritePropertyName("musig_participant_pubkeys");
929+
jsonWriter.WriteStartObject();
930+
foreach (var o in MusigParticipantPubKeys)
931+
{
932+
jsonWriter.WritePropertyName(o.Key.ToHex());
933+
jsonWriter.WriteStartArray();
934+
foreach (var k in o.Value)
935+
{
936+
jsonWriter.WriteValue(k.ToHex());
937+
}
938+
jsonWriter.WriteEndArray();
939+
}
940+
jsonWriter.WriteEndObject();
941+
}
942+
943+
if (MusigPartialSigs.Count != 0)
944+
{
945+
jsonWriter.WritePropertyName("musig_partial_signatures");
946+
jsonWriter.WriteStartObject();
947+
foreach (var sig in MusigPartialSigs)
948+
{
949+
jsonWriter.WritePropertyValue(Encoders.Hex.EncodeData(sig.Key.ToBytes(0)[1..]), Encoders.Hex.EncodeData(sig.Value));
950+
}
951+
jsonWriter.WriteEndObject();
952+
}
953+
if (MusigPartialSigs.Count != 0)
954+
{
955+
jsonWriter.WritePropertyName("musig_pub_nonces");
956+
jsonWriter.WriteStartObject();
957+
foreach (var sig in MusigPubNonces)
958+
{
959+
jsonWriter.WritePropertyValue(Encoders.Hex.EncodeData(sig.Key.ToBytes(0)[1..]), Encoders.Hex.EncodeData(sig.Value));
960+
}
961+
jsonWriter.WriteEndObject();
962+
}
963+
#endif
866964
if (sighash_type is uint s)
867965
jsonWriter.WritePropertyValue("sighash", GetName(s));
868966
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)