Skip to content

Commit 76c466c

Browse files
committed
Derive MuSigPublicKeys for DLCs avoiding key reuse
1 parent a4f567e commit 76c466c

File tree

6 files changed

+179
-100
lines changed

6 files changed

+179
-100
lines changed

coinlib/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ secp256k1-coinlib fork.
1515
inclusion.
1616
- Moves to underlying secp256k1-coinlib.
1717
- Removed dependency to wasm_interop that had a broken js dependency.
18+
- `Writable.toBytes()` returns a copy of the cached bytes to avoid mutation.
1819
- Fixes `extraEntropy` being ignored for `Secp256k1Base.schnorrSign`.
1920

2021
## 4.1.0

coinlib/lib/src/common/serial.dart

Lines changed: 12 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import 'dart:typed_data';
22
import 'package:coinlib/src/common/hex.dart';
3-
import 'package:coinlib/src/crypto/ec_compressed_public_key.dart';
4-
import 'package:coinlib/src/crypto/ec_public_key.dart';
53
import 'package:coinlib/src/tx/locktime.dart';
6-
74
import 'checks.dart';
85

96
/// Thrown when attempting to read or write beyond the boundary of data
@@ -97,22 +94,6 @@ class BytesReader extends _ReadWriteBase {
9794
List<T> readListWithFunc<T>(T Function() read)
9895
=> List<T>.generate(readVarInt().toInt(), (_) => read());
9996

100-
/// Reads a list of public keys that are in compressed format.
101-
List<ECPublicKey> readPubKeyVector() => readListWithFunc(
102-
() => ECPublicKey(readSlice(33)),
103-
);
104-
105-
Map<K, V> readMap<K, V>(K Function() readKey, V Function() readValue)
106-
=> Map.fromEntries(
107-
Iterable.generate(
108-
readVarInt().toInt(),
109-
(_) => MapEntry(readKey(), readValue()),
110-
),
111-
);
112-
113-
Map<ECPublicKey, V> readPubKeyMap<V>(V Function() readValue)
114-
=> readMap(() => ECPublicKey(readSlice(33)), readValue);
115-
11697
Locktime readLocktime() => Locktime(readUInt32());
11798

11899
}
@@ -153,37 +134,6 @@ mixin Writer {
153134
}
154135
}
155136

156-
/// Writes a list of public keys. They will be converted to compressed format
157-
/// if necessary.
158-
void writePubKeyVector(List<ECPublicKey> keys) => writeListWithFunc(
159-
keys, (key) => writeSlice(ECCompressedPublicKey.fromPubkey(key).data),
160-
);
161-
162-
/// Writes a map serialising the keys and values using [writeKey] and
163-
/// [writeValue].
164-
void writeMap<K, V>(
165-
Map<K, V> map,
166-
void Function(K) writeKey,
167-
void Function(V) writeValue,
168-
) {
169-
writeVarInt(BigInt.from(map.length));
170-
for (final entry in map.entries) {
171-
writeKey(entry.key);
172-
writeValue(entry.value);
173-
}
174-
}
175-
176-
/// Writes a map using public keys as map keys which will be converted into
177-
/// compressed format if necessary.
178-
void writePubKeyMap<V>(
179-
Map<ECPublicKey, V> map,
180-
void Function(V) writeValue,
181-
) => writeMap(
182-
map,
183-
(key) => writeSlice(ECCompressedPublicKey.fromPubkey(key).data),
184-
writeValue,
185-
);
186-
187137
/// Writes a vector of all the writable elements
188138
void writeWritableVector(List<Writable> list) {
189139
writeVarInt(BigInt.from(list.length));
@@ -316,14 +266,19 @@ mixin Writable {
316266
/// Override to write data into [writer]
317267
void write(Writer writer);
318268

319-
/// Obtains a cached [Uint8List] with data serialized for this object
269+
/// Obtains a copy of a [Uint8List] with data serialized for this
270+
/// object. The serialisation is cached allowing this to be called multiple
271+
/// times without writing from the object each time.
320272
Uint8List toBytes() {
321-
if (_cache != null) return _cache!;
322-
final bytes = Uint8List(size);
323-
final writer = BytesWriter(bytes);
324-
write(writer);
325-
_sizeCache = bytes.length;
326-
return _cache = bytes;
273+
274+
if (_cache == null) {
275+
_cache = Uint8List(size);
276+
write(BytesWriter(_cache!));
277+
_sizeCache = _cache!.length;
278+
}
279+
280+
return Uint8List.fromList(_cache!);
281+
327282
}
328283

329284
String toHex() => bytesToHex(toBytes());

coinlib/lib/src/dlc/terms.dart

Lines changed: 98 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import 'dart:typed_data';
2+
import 'package:coinlib/src/common/bytes.dart';
23
import 'package:coinlib/src/common/hex.dart';
34
import 'package:coinlib/src/common/serial.dart';
45
import 'package:coinlib/src/crypto/ec_public_key.dart';
6+
import 'package:coinlib/src/crypto/hash.dart';
7+
import 'package:coinlib/src/musig/library.dart';
58
import 'package:coinlib/src/network.dart';
69
import 'package:coinlib/src/tx/locktime.dart';
710
import 'package:coinlib/src/tx/output.dart';
811
import 'package:coinlib/src/tx/transaction.dart';
12+
import 'package:collection/collection.dart';
913

1014
class 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.
3374
class 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
}

coinlib/lib/src/taproot/taproot.dart

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import 'package:coinlib/src/crypto/ec_private_key.dart';
55
import 'package:coinlib/src/crypto/ec_public_key.dart';
66
import 'package:coinlib/src/crypto/hash.dart';
77
import 'package:coinlib/src/scripts/script.dart';
8-
import 'package:collection/collection.dart';
98

109
/// This class encapsulates the construction of Taproot tweaked keys given an
1110
/// internal key and MAST consisting of Tapscript leaves constructed with
@@ -135,16 +134,7 @@ class TapBranch implements TapNode {
135134

136135
// Used to determine which hash should be encoded first. The smallest hash
137136
// should be first.
138-
bool _leftFirst() {
139-
140-
for (final pair in IterableZip([l.hash, r.hash])) {
141-
if (pair[0] < pair[1]) return true;
142-
if (pair[0] > pair[1]) return false;
143-
}
144-
145-
return true;
146-
147-
}
137+
bool _leftFirst() => compareBytes(l.hash, r.hash) <= 0;
148138

149139
Uint8List? _hashCache;
150140
@override

coinlib/test/common/serial_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,8 @@ void main() {
278278
// Should work OK twice
279279
final obj = WritableTestTx();
280280
for (int i = 0; i < 2; i++) {
281+
// Should copy bytes with no mutation to underlying
282+
obj.toBytes()[0] = 0xff;
281283
expect(obj.toBytes(), expData);
282284
expect(obj.toHex(), bytesToHex(expData));
283285
expect(obj.size, expSize);

0 commit comments

Comments
 (0)