11import 'dart:typed_data' ;
2+ import 'package:coinlib/src/common/bytes.dart' ;
23import 'package:coinlib/src/common/hex.dart' ;
34import 'package:coinlib/src/common/serial.dart' ;
45import 'package:coinlib/src/crypto/ec_public_key.dart' ;
6+ import 'package:coinlib/src/crypto/hash.dart' ;
7+ import 'package:coinlib/src/musig/library.dart' ;
58import 'package:coinlib/src/network.dart' ;
69import 'package:coinlib/src/tx/locktime.dart' ;
710import 'package:coinlib/src/tx/output.dart' ;
811import 'package:coinlib/src/tx/transaction.dart' ;
12+ import 'package:collection/collection.dart' ;
913
1014class InvalidDLCTerms implements Exception {
1115
@@ -21,13 +25,50 @@ class InvalidDLCTerms implements Exception {
2125 : this ("Contains output value less than min of $min " );
2226 InvalidDLCTerms .smallFunding (BigInt min)
2327 : this ("Contains funding value less than min of $min " );
28+ InvalidDLCTerms .notOrdered ()
29+ : this ("The input bytes contain out-of-order keys" );
2430
2531}
2632
27- BigInt addBigInts (Iterable <BigInt > ints) => ints.fold (
33+ BigInt _addBigInts (Iterable <BigInt > ints) => ints.fold (
2834 BigInt .zero, (a, b) => a+ b,
2935);
3036
37+ Map <ECPublicKey , T > _xOnlyUnmodifiableMap <T >(Map <ECPublicKey , T > map)
38+ => Map .unmodifiable (map.map ((key, v) => MapEntry (key.xonly, v)));
39+
40+ Map <ECPublicKey , T > _readPubKeyMap <T >(
41+ BytesReader reader,
42+ T Function () readValue,
43+ ) => Map .fromEntries (
44+ Iterable .generate (
45+ reader.readVarInt ().toInt (),
46+ (_) => MapEntry (
47+ ECPublicKey .fromXOnly (reader.readSlice (32 )),
48+ readValue (),
49+ ),
50+ ),
51+ );
52+
53+ void _writeOrderedPubkeyMap <T >(
54+ Writer writer,
55+ Map <ECPublicKey , T > map,
56+ void Function (T ) writeValue,
57+ ) {
58+
59+ writer.writeVarInt (BigInt .from (map.length));
60+
61+ final orderedEntries = map.entries
62+ .map ((entry) => MapEntry (entry.key.x, entry.value))
63+ .sortedByCompare ((entry) => entry.key, compareBytes);
64+
65+ for (final entry in orderedEntries) {
66+ writer.writeSlice (entry.key);
67+ writeValue (entry.value);
68+ }
69+
70+ }
71+
3172/// A CET will pay to the [outputs] with the value of each output evenly reduced
3273/// to cover the transaction fee.
3374class CETOutputs {
@@ -53,7 +94,7 @@ class CETOutputs {
5394 }
5495 }
5596
56- BigInt get totalValue => addBigInts (outputs.map ((out) => out.value));
97+ BigInt get totalValue => _addBigInts (outputs.map ((out) => out.value));
5798
5899}
59100
@@ -72,8 +113,8 @@ class DLCTerms with Writable {
72113 /// The version of the protocol is currently 1
73114 static final int version = 1 ;
74115
75- /// A list of participants that must sign all CETs and RTs for the DLCs.
76- final List <ECPublicKey > participants;
116+ /// A set of participants that must sign all CETs and RTs for the DLCs.
117+ final Set <ECPublicKey > participants;
77118
78119 /// How much each participant is expected to fund the DLC. A public key may
79120 /// refer to a funder outside of [participants] if they are not expected to
@@ -105,17 +146,18 @@ class DLCTerms with Writable {
105146 /// broadcast of a CET, but this is not checked.
106147 final Locktime refundLocktime;
107148
108- /// May throw [InvalidDLCTerms] .
149+ /// All [ECPublicKey] s will be coerced into x-only public keys. May throw
150+ /// [InvalidDLCTerms] .
109151 DLCTerms ({
110- required List <ECPublicKey > participants,
152+ required Set <ECPublicKey > participants,
111153 required Map <ECPublicKey , BigInt > fundAmounts,
112154 required Map <ECPublicKey , CETOutputs > outcomes,
113155 required this .refundLocktime,
114156 required Network network,
115157 }) :
116- participants = List .unmodifiable (participants),
117- fundAmounts = Map . unmodifiable (fundAmounts),
118- outcomes = Map . unmodifiable (outcomes) {
158+ participants = Set .unmodifiable (participants. map ((key) => key.xonly) ),
159+ fundAmounts = _xOnlyUnmodifiableMap (fundAmounts),
160+ outcomes = _xOnlyUnmodifiableMap (outcomes) {
119161
120162 // There should not be any funding amount for a participant which is under
121163 // the minimum output
@@ -124,7 +166,7 @@ class DLCTerms with Writable {
124166 }
125167
126168 // The outcome output amounts must add up to the total funded amount
127- final totalToFund = addBigInts (fundAmounts.values);
169+ final totalToFund = _addBigInts (fundAmounts.values);
128170 if (
129171 outcomes.values.any (
130172 (outcome) => outcome.totalValue.compareTo (totalToFund) != 0 ,
@@ -146,10 +188,14 @@ class DLCTerms with Writable {
146188 throw InvalidDLCTerms .badVersion (version);
147189 }
148190
149- return DLCTerms (
150- participants: reader.readPubKeyVector (),
151- fundAmounts: reader.readPubKeyMap (() => reader.readVarInt ()),
152- outcomes: reader.readPubKeyMap (
191+ final terms = DLCTerms (
192+ participants: Iterable .generate (
193+ reader.readVarInt ().toInt (),
194+ (_) => ECPublicKey .fromXOnly (reader.readSlice (32 )),
195+ ).toSet (),
196+ fundAmounts: _readPubKeyMap (reader, () => reader.readVarInt ()),
197+ outcomes: _readPubKeyMap (
198+ reader,
153199 () => CETOutputs (
154200 reader.readListWithFunc (() => Output .fromReader (reader)),
155201 network,
@@ -159,6 +205,15 @@ class DLCTerms with Writable {
159205 network: network,
160206 );
161207
208+ // Check public keys were ordered correctly
209+ // This is not the optimal way to do this but is simple
210+ final inBytes = reader.bytes.buffer.asUint8List ();
211+ if (compareBytes (inBytes, terms.toBytes ()) != 0 ) {
212+ throw InvalidDLCTerms .notOrdered ();
213+ }
214+
215+ return terms;
216+
162217 }
163218
164219 factory DLCTerms .fromBytes (Uint8List bytes, Network network)
@@ -168,19 +223,43 @@ class DLCTerms with Writable {
168223 => DLCTerms .fromBytes (hexToBytes (hex), network);
169224
170225 @override
171- /// The public keys will be written as compressed public keys
172226 void write (Writer writer) {
227+
173228 writer.writeUInt16 (version);
174- writer.writePubKeyVector (participants);
175- writer.writePubKeyMap (
229+
230+ // Sort the public keys ensuring the written set is always the same
231+ writer.writeVarInt (BigInt .from (participants.length));
232+ for (final key in participants.map ((key) => key.x).sorted (compareBytes)) {
233+ writer.writeSlice (key);
234+ }
235+
236+ _writeOrderedPubkeyMap (
237+ writer,
176238 fundAmounts,
177- (amount ) => writer.writeVarInt (amount ),
239+ (amt ) => writer.writeVarInt (amt ),
178240 );
179- writer.writePubKeyMap (
241+
242+ _writeOrderedPubkeyMap (
243+ writer,
180244 outcomes,
181245 (outputs) => writer.writeWritableVector (outputs.outputs),
182246 );
247+
183248 writer.writeLocktime (refundLocktime);
249+
184250 }
185251
252+ // Use a tagged hasher to avoid potential conflicts that could lead to key
253+ // reuse
254+ static final _dlcKeyTweakHash = getTaggedHasher ("CoinlibDLCKeyTweak" );
255+ Uint8List ? _tweakHashCache;
256+
257+ /// Obtains the tweaked MuSig2 aggregate key for this DLC. The key is
258+ /// aggregated from the [participants] and then tweaked from the [DLCTerms]
259+ /// data to prevent key-reuse across multiple DLCs in the event that
260+ /// participants re-use their individual keys.
261+ MuSigPublicKeys get musig => MuSigPublicKeys (participants).tweak (
262+ _tweakHashCache ?? = _dlcKeyTweakHash (toBytes ()),
263+ );
264+
186265}
0 commit comments