Skip to content

Commit a4f567e

Browse files
committed
Add DLCTerms
1 parent 3d18770 commit a4f567e

File tree

6 files changed

+418
-11
lines changed

6 files changed

+418
-11
lines changed

coinlib/lib/src/coinlib_base.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export 'package:coinlib/src/crypto/random.dart';
1717
export 'package:coinlib/src/crypto/schnorr_adaptor_signature.dart';
1818
export 'package:coinlib/src/crypto/schnorr_signature.dart';
1919

20+
export 'package:coinlib/src/dlc/terms.dart';
21+
2022
export 'package:coinlib/src/encode/base58.dart';
2123
export 'package:coinlib/src/encode/bech32.dart';
2224
export 'package:coinlib/src/encode/wif.dart';

coinlib/lib/src/common/serial.dart

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
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';
5+
import 'package:coinlib/src/tx/locktime.dart';
36

47
import 'checks.dart';
58

@@ -91,6 +94,27 @@ class BytesReader extends _ReadWriteBase {
9194
List<Uint8List> readVector()
9295
=> List<Uint8List>.generate(readVarInt().toInt(), (i) => readVarSlice());
9396

97+
List<T> readListWithFunc<T>(T Function() read)
98+
=> List<T>.generate(readVarInt().toInt(), (_) => read());
99+
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+
116+
Locktime readLocktime() => Locktime(readUInt32());
117+
94118
}
95119

96120
/// Methods to handle the writing of data
@@ -120,6 +144,56 @@ mixin Writer {
120144
}
121145
}
122146

147+
/// Writes elements from the [list] using the [write] function on each
148+
/// element.
149+
void writeListWithFunc<T>(List<T> list, void Function(T) write) {
150+
writeVarInt(BigInt.from(list.length));
151+
for (final el in list) {
152+
write(el);
153+
}
154+
}
155+
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+
187+
/// Writes a vector of all the writable elements
188+
void writeWritableVector(List<Writable> list) {
189+
writeVarInt(BigInt.from(list.length));
190+
for (final el in list) {
191+
el.write(this);
192+
}
193+
}
194+
195+
void writeLocktime(Locktime locktime) => writeUInt32(locktime.value);
196+
123197
}
124198

125199
/// Writes serialized data to a Uint8List. Throws an [OutOfData] exception if

coinlib/lib/src/dlc/terms.dart

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import 'dart:typed_data';
2+
import 'package:coinlib/src/common/hex.dart';
3+
import 'package:coinlib/src/common/serial.dart';
4+
import 'package:coinlib/src/crypto/ec_public_key.dart';
5+
import 'package:coinlib/src/network.dart';
6+
import 'package:coinlib/src/tx/locktime.dart';
7+
import 'package:coinlib/src/tx/output.dart';
8+
import 'package:coinlib/src/tx/transaction.dart';
9+
10+
class InvalidDLCTerms implements Exception {
11+
12+
final String message;
13+
14+
InvalidDLCTerms(this.message);
15+
InvalidDLCTerms.badOutcomeMatch()
16+
: this("Contains outcome output amounts not matching the funded amount");
17+
InvalidDLCTerms.badVersion(int v)
18+
: this("Version $v isn't allowed. Only v1 is supported.");
19+
InvalidDLCTerms.noOutputs() : this("CETOutputs have no outputs");
20+
InvalidDLCTerms.smallOutput(BigInt min)
21+
: this("Contains output value less than min of $min");
22+
InvalidDLCTerms.smallFunding(BigInt min)
23+
: this("Contains funding value less than min of $min");
24+
25+
}
26+
27+
BigInt addBigInts(Iterable<BigInt> ints) => ints.fold(
28+
BigInt.zero, (a, b) => a+b,
29+
);
30+
31+
/// A CET will pay to the [outputs] with the value of each output evenly reduced
32+
/// to cover the transaction fee.
33+
class CETOutputs {
34+
35+
/// The outputs to be included in the CET of this outcome. The values must
36+
/// add up to the amounts being funded by the participants in
37+
/// [DLCTerms.fundAmounts].
38+
///
39+
/// The [Output.value] for each output will have an equal share of the
40+
/// transaction fee removed when the transaction is constructed. When doing
41+
/// this, if any of the outputs fall below the dust amount, they will be
42+
/// removed first.
43+
final List<Output> outputs;
44+
45+
/// Requires that the output values are at least [Network.minOutput] or
46+
/// [InvalidDLCTerms] may be thrown.
47+
CETOutputs(this.outputs, Network network) {
48+
if (outputs.isEmpty) {
49+
throw InvalidDLCTerms.noOutputs();
50+
}
51+
if (outputs.any((out) => out.value.compareTo(network.minOutput) < 0)) {
52+
throw InvalidDLCTerms.smallOutput(network.minOutput);
53+
}
54+
}
55+
56+
BigInt get totalValue => addBigInts(outputs.map((out) => out.value));
57+
58+
}
59+
60+
/// Specifies the terms of a DLC contract to be agreed upon by all
61+
/// [participants].
62+
///
63+
/// DLCs use CETs signed with adaptor signatures to be completed using an
64+
/// oracle. CETs do not have a refund mechanism as they can only be broadcast
65+
/// when the oracle reveals the associated scalar.
66+
///
67+
/// Funding transactions are only created after all CETs and a Refund
68+
/// Transaction have been created. APO is used to allow creation of the CETs and
69+
/// the Refund Transaction before the Funding Transaction.
70+
class DLCTerms with Writable {
71+
72+
/// The version of the protocol is currently 1
73+
static final int version = 1;
74+
75+
/// A list of participants that must sign all CETs and RTs for the DLCs.
76+
final List<ECPublicKey> participants;
77+
78+
/// How much each participant is expected to fund the DLC. A public key may
79+
/// refer to a funder outside of [participants] if they are not expected to
80+
/// sign.
81+
///
82+
/// Note that these participants will also be expected to pay an equal
83+
/// contribution to the Funding Transaction fee in excess of these amounts.
84+
final Map<ECPublicKey, BigInt> fundAmounts;
85+
86+
/// Maps oracle adaptor points to [CETOutputs] that contain the output
87+
/// information to include in Contract Execution Transactions.
88+
///
89+
/// The points can be arbitrarily announced by the oracle in association with
90+
/// each outcome.
91+
///
92+
/// Alternatively, as proposed in the original DLC designs, a point can be
93+
/// the S point of an oracle signature where `S = R + eP`, and where `R` is a
94+
/// pre-disclosed nonce point, `P` is the oracle public key and `e` is the
95+
/// commitment of the nonce and message representing the outcome. This allows
96+
/// computation of multiple adaptor points for multiple outcome messages given
97+
/// the R and P points. coinlib doesn't provide an abstraction for
98+
/// constructing adaptor points via signatures this way.
99+
final Map<ECPublicKey, CETOutputs> outcomes;
100+
101+
/// The [Transaction.locktime] to be used in the Refund Transaction where
102+
/// participants may regain access to funds.
103+
///
104+
/// Ought to be in the future to give enough time for the oracle event and
105+
/// broadcast of a CET, but this is not checked.
106+
final Locktime refundLocktime;
107+
108+
/// May throw [InvalidDLCTerms].
109+
DLCTerms({
110+
required List<ECPublicKey> participants,
111+
required Map<ECPublicKey, BigInt> fundAmounts,
112+
required Map<ECPublicKey, CETOutputs> outcomes,
113+
required this.refundLocktime,
114+
required Network network,
115+
}) :
116+
participants = List.unmodifiable(participants),
117+
fundAmounts = Map.unmodifiable(fundAmounts),
118+
outcomes = Map.unmodifiable(outcomes) {
119+
120+
// There should not be any funding amount for a participant which is under
121+
// the minimum output
122+
if (fundAmounts.values.any((val) => val.compareTo(network.minOutput) < 0)) {
123+
throw InvalidDLCTerms.smallFunding(network.minOutput);
124+
}
125+
126+
// The outcome output amounts must add up to the total funded amount
127+
final totalToFund = addBigInts(fundAmounts.values);
128+
if (
129+
outcomes.values.any(
130+
(outcome) => outcome.totalValue.compareTo(totalToFund) != 0,
131+
)
132+
) {
133+
throw InvalidDLCTerms.badOutcomeMatch();
134+
}
135+
136+
}
137+
138+
/// There are no size limits, so the caller may wish to enforce a reasonable
139+
/// size for the serialised data. Public keys will always be serialised and
140+
/// read as compressed keys.
141+
///
142+
/// May throw [InvalidDLCTerms].
143+
factory DLCTerms.fromReader(BytesReader reader, Network network) {
144+
145+
if (reader.readUInt16() != version) {
146+
throw InvalidDLCTerms.badVersion(version);
147+
}
148+
149+
return DLCTerms(
150+
participants: reader.readPubKeyVector(),
151+
fundAmounts: reader.readPubKeyMap(() => reader.readVarInt()),
152+
outcomes: reader.readPubKeyMap(
153+
() => CETOutputs(
154+
reader.readListWithFunc(() => Output.fromReader(reader)),
155+
network,
156+
),
157+
),
158+
refundLocktime: reader.readLocktime(),
159+
network: network,
160+
);
161+
162+
}
163+
164+
factory DLCTerms.fromBytes(Uint8List bytes, Network network)
165+
=> DLCTerms.fromReader(BytesReader(bytes), network);
166+
167+
factory DLCTerms.fromHex(String hex, Network network)
168+
=> DLCTerms.fromBytes(hexToBytes(hex), network);
169+
170+
@override
171+
/// The public keys will be written as compressed public keys
172+
void write(Writer writer) {
173+
writer.writeUInt16(version);
174+
writer.writePubKeyVector(participants);
175+
writer.writePubKeyMap(
176+
fundAmounts,
177+
(amount) => writer.writeVarInt(amount),
178+
);
179+
writer.writePubKeyMap(
180+
outcomes,
181+
(outputs) => writer.writeWritableVector(outputs.outputs),
182+
);
183+
writer.writeLocktime(refundLocktime);
184+
}
185+
186+
}

coinlib/lib/src/tx/transaction.dart

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -169,23 +169,16 @@ class Transaction with Writable {
169169
writer.writeUInt8(1); // Flag
170170
}
171171

172-
writer.writeVarInt(BigInt.from(inputs.length));
173-
for (final input in inputs) {
174-
input.write(writer);
175-
}
176-
177-
writer.writeVarInt(BigInt.from(outputs.length));
178-
for (final output in outputs) {
179-
output.write(writer);
180-
}
172+
writer.writeWritableVector(inputs);
173+
writer.writeWritableVector(outputs);
181174

182175
if (isWitness) {
183176
for (final input in inputs) {
184177
writer.writeVector(input is WitnessInput ? input.witness : []);
185178
}
186179
}
187180

188-
writer.writeUInt32(locktime.value);
181+
writer.writeLocktime(locktime);
189182

190183
}
191184

coinlib/test/common/serial_test.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ class WritableTestTx with Writable {
6060

6161
void main() {
6262

63-
6463
group("BytesReader", () {
6564

6665
test("can read tx", () {

0 commit comments

Comments
 (0)