Skip to content

Commit 607dbcb

Browse files
committed
Seperate DLC outcomes with locktime and add Taproot construction
1 parent 76c466c commit 607dbcb

10 files changed

Lines changed: 211 additions & 101 deletions

File tree

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/errors.dart';
21+
export 'package:coinlib/src/dlc/outcome.dart';
2022
export 'package:coinlib/src/dlc/terms.dart';
2123

2224
export 'package:coinlib/src/encode/base58.dart';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
BigInt addBigInts(Iterable<BigInt> ints) => ints.fold(
2+
BigInt.zero, (a, b) => a+b,
3+
);

coinlib/lib/src/dlc/errors.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
class InvalidDLCTerms implements Exception {
2+
3+
final String message;
4+
5+
InvalidDLCTerms(this.message);
6+
InvalidDLCTerms.badOutcomeMatch()
7+
: this("Contains outcome output amounts not matching the funded amount");
8+
InvalidDLCTerms.badVersion(int v)
9+
: this("Version $v isn't allowed. Only v1 is supported.");
10+
InvalidDLCTerms.noOutputs() : this("CETOutcome have no outputs");
11+
InvalidDLCTerms.smallOutput(BigInt min)
12+
: this("Contains output value less than min of $min");
13+
InvalidDLCTerms.smallFunding(BigInt min)
14+
: this("Contains funding value less than min of $min");
15+
InvalidDLCTerms.notOrdered()
16+
: this("The input bytes contain out-of-order keys");
17+
InvalidDLCTerms.cetLocktimeAfterRf()
18+
: this("A CET locktime is not before the RF locktime");
19+
20+
}

coinlib/lib/src/dlc/outcome.dart

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import 'package:coinlib/src/common/bigints.dart';
2+
import 'package:coinlib/src/common/serial.dart';
3+
import 'package:coinlib/src/network.dart';
4+
import 'package:coinlib/src/tx/locktime.dart';
5+
import 'package:coinlib/src/tx/output.dart';
6+
import 'terms.dart';
7+
import 'errors.dart';
8+
9+
/// A CET will pay to the [outputs] with the value of each output evenly reduced
10+
/// to cover the transaction fee.
11+
class CETOutcome with Writable {
12+
13+
/// The outputs to be included in the CET of this outcome. The values must
14+
/// add up to the amounts being funded by the participants in
15+
/// [DLCTerms.fundAmounts].
16+
///
17+
/// The [Output.value] for each output will have an equal share of the
18+
/// transaction fee removed when the transaction is constructed. When doing
19+
/// this, if any of the outputs fall below the dust amount, they will be
20+
/// removed first.
21+
final List<Output> outputs;
22+
23+
/// The locktime to use for this CET. It should be no later than when the
24+
/// oracle is expected to reveal the discrete log and must be the same type as
25+
/// (block height or median timestamp) and before the Refund Transaction
26+
/// locktime.
27+
///
28+
/// How long before the RF is not decided by this library but it should be
29+
/// sufficient time to broadcast and include the CET in a block before the RF
30+
/// becomes available.
31+
final Locktime locktime;
32+
33+
/// Requires that the output values are at least [Network.minOutput] or
34+
/// [InvalidDLCTerms] may be thrown.
35+
CETOutcome({
36+
required this.outputs,
37+
required this.locktime,
38+
required Network network,
39+
}) {
40+
if (outputs.isEmpty) {
41+
throw InvalidDLCTerms.noOutputs();
42+
}
43+
if (outputs.any((out) => out.value.compareTo(network.minOutput) < 0)) {
44+
throw InvalidDLCTerms.smallOutput(network.minOutput);
45+
}
46+
}
47+
48+
CETOutcome.fromReader(BytesReader reader, Network network) : this(
49+
outputs: reader.readListWithFunc(() => Output.fromReader(reader)),
50+
locktime: reader.readLocktime(),
51+
network: network,
52+
);
53+
54+
BigInt get totalValue => addBigInts(outputs.map((out) => out.value));
55+
56+
@override
57+
void write(Writer writer) {
58+
writer.writeWritableVector(outputs);
59+
writer.writeLocktime(locktime);
60+
}
61+
62+
}

coinlib/lib/src/dlc/terms.dart

Lines changed: 31 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,19 @@
11
import 'dart:typed_data';
2+
import 'package:coinlib/src/common/bigints.dart';
23
import 'package:coinlib/src/common/bytes.dart';
34
import 'package:coinlib/src/common/hex.dart';
45
import 'package:coinlib/src/common/serial.dart';
56
import 'package:coinlib/src/crypto/ec_public_key.dart';
67
import 'package:coinlib/src/crypto/hash.dart';
78
import 'package:coinlib/src/musig/library.dart';
89
import 'package:coinlib/src/network.dart';
10+
import 'package:coinlib/src/taproot/leaves.dart';
11+
import 'package:coinlib/src/taproot/taproot.dart';
912
import 'package:coinlib/src/tx/locktime.dart';
10-
import 'package:coinlib/src/tx/output.dart';
1113
import 'package:coinlib/src/tx/transaction.dart';
1214
import 'package:collection/collection.dart';
13-
14-
class InvalidDLCTerms implements Exception {
15-
16-
final String message;
17-
18-
InvalidDLCTerms(this.message);
19-
InvalidDLCTerms.badOutcomeMatch()
20-
: this("Contains outcome output amounts not matching the funded amount");
21-
InvalidDLCTerms.badVersion(int v)
22-
: this("Version $v isn't allowed. Only v1 is supported.");
23-
InvalidDLCTerms.noOutputs() : this("CETOutputs have no outputs");
24-
InvalidDLCTerms.smallOutput(BigInt min)
25-
: this("Contains output value less than min of $min");
26-
InvalidDLCTerms.smallFunding(BigInt min)
27-
: this("Contains funding value less than min of $min");
28-
InvalidDLCTerms.notOrdered()
29-
: this("The input bytes contain out-of-order keys");
30-
31-
}
32-
33-
BigInt _addBigInts(Iterable<BigInt> ints) => ints.fold(
34-
BigInt.zero, (a, b) => a+b,
35-
);
15+
import 'outcome.dart';
16+
import 'errors.dart';
3617

3718
Map<ECPublicKey, T> _xOnlyUnmodifiableMap<T>(Map<ECPublicKey, T> map)
3819
=> Map.unmodifiable(map.map((key, v) => MapEntry(key.xonly, v)));
@@ -69,35 +50,6 @@ void _writeOrderedPubkeyMap<T>(
6950

7051
}
7152

72-
/// A CET will pay to the [outputs] with the value of each output evenly reduced
73-
/// to cover the transaction fee.
74-
class CETOutputs {
75-
76-
/// The outputs to be included in the CET of this outcome. The values must
77-
/// add up to the amounts being funded by the participants in
78-
/// [DLCTerms.fundAmounts].
79-
///
80-
/// The [Output.value] for each output will have an equal share of the
81-
/// transaction fee removed when the transaction is constructed. When doing
82-
/// this, if any of the outputs fall below the dust amount, they will be
83-
/// removed first.
84-
final List<Output> outputs;
85-
86-
/// Requires that the output values are at least [Network.minOutput] or
87-
/// [InvalidDLCTerms] may be thrown.
88-
CETOutputs(this.outputs, Network network) {
89-
if (outputs.isEmpty) {
90-
throw InvalidDLCTerms.noOutputs();
91-
}
92-
if (outputs.any((out) => out.value.compareTo(network.minOutput) < 0)) {
93-
throw InvalidDLCTerms.smallOutput(network.minOutput);
94-
}
95-
}
96-
97-
BigInt get totalValue => _addBigInts(outputs.map((out) => out.value));
98-
99-
}
100-
10153
/// Specifies the terms of a DLC contract to be agreed upon by all
10254
/// [participants].
10355
///
@@ -124,7 +76,7 @@ class DLCTerms with Writable {
12476
/// contribution to the Funding Transaction fee in excess of these amounts.
12577
final Map<ECPublicKey, BigInt> fundAmounts;
12678

127-
/// Maps oracle adaptor points to [CETOutputs] that contain the output
79+
/// Maps oracle adaptor points to [CETOutcome] that contain the output
12880
/// information to include in Contract Execution Transactions.
12981
///
13082
/// The points can be arbitrarily announced by the oracle in association with
@@ -137,7 +89,7 @@ class DLCTerms with Writable {
13789
/// computation of multiple adaptor points for multiple outcome messages given
13890
/// the R and P points. coinlib doesn't provide an abstraction for
13991
/// constructing adaptor points via signatures this way.
140-
final Map<ECPublicKey, CETOutputs> outcomes;
92+
final Map<ECPublicKey, CETOutcome> outcomes;
14193

14294
/// The [Transaction.locktime] to be used in the Refund Transaction where
14395
/// participants may regain access to funds.
@@ -151,7 +103,7 @@ class DLCTerms with Writable {
151103
DLCTerms({
152104
required Set<ECPublicKey> participants,
153105
required Map<ECPublicKey, BigInt> fundAmounts,
154-
required Map<ECPublicKey, CETOutputs> outcomes,
106+
required Map<ECPublicKey, CETOutcome> outcomes,
155107
required this.refundLocktime,
156108
required Network network,
157109
}) :
@@ -166,7 +118,7 @@ class DLCTerms with Writable {
166118
}
167119

168120
// The outcome output amounts must add up to the total funded amount
169-
final totalToFund = _addBigInts(fundAmounts.values);
121+
final totalToFund = addBigInts(fundAmounts.values);
170122
if (
171123
outcomes.values.any(
172124
(outcome) => outcome.totalValue.compareTo(totalToFund) != 0,
@@ -175,6 +127,14 @@ class DLCTerms with Writable {
175127
throw InvalidDLCTerms.badOutcomeMatch();
176128
}
177129

130+
if (
131+
outcomes.values.any(
132+
(outcome) => !outcome.locktime.isDefinitelyBefore(refundLocktime),
133+
)
134+
) {
135+
throw InvalidDLCTerms.cetLocktimeAfterRf();
136+
}
137+
178138
}
179139

180140
/// There are no size limits, so the caller may wish to enforce a reasonable
@@ -196,10 +156,7 @@ class DLCTerms with Writable {
196156
fundAmounts: _readPubKeyMap(reader, () => reader.readVarInt()),
197157
outcomes: _readPubKeyMap(
198158
reader,
199-
() => CETOutputs(
200-
reader.readListWithFunc(() => Output.fromReader(reader)),
201-
network,
202-
),
159+
() => CETOutcome.fromReader(reader, network),
203160
),
204161
refundLocktime: reader.readLocktime(),
205162
network: network,
@@ -242,7 +199,7 @@ class DLCTerms with Writable {
242199
_writeOrderedPubkeyMap(
243200
writer,
244201
outcomes,
245-
(outputs) => writer.writeWritableVector(outputs.outputs),
202+
(outcome) => outcome.write(writer),
246203
);
247204

248205
writer.writeLocktime(refundLocktime);
@@ -252,14 +209,23 @@ class DLCTerms with Writable {
252209
// Use a tagged hasher to avoid potential conflicts that could lead to key
253210
// reuse
254211
static final _dlcKeyTweakHash = getTaggedHasher("CoinlibDLCKeyTweak");
255-
Uint8List? _tweakHashCache;
212+
213+
MuSigPublicKeys? _muSigCache;
256214

257215
/// Obtains the tweaked MuSig2 aggregate key for this DLC. The key is
258216
/// aggregated from the [participants] and then tweaked from the [DLCTerms]
259217
/// data to prevent key-reuse across multiple DLCs in the event that
260218
/// participants re-use their individual keys.
261-
MuSigPublicKeys get musig => MuSigPublicKeys(participants).tweak(
262-
_tweakHashCache ??= _dlcKeyTweakHash(toBytes()),
219+
MuSigPublicKeys get musig
220+
=> _muSigCache ??= MuSigPublicKeys(participants).tweak(
221+
_dlcKeyTweakHash(toBytes()),
222+
);
223+
224+
/// Obtains [Taproot] allowing key-path spend using the [musig] key or an APO
225+
/// CHECKSIG script-path using the same key used by the CETs and RF.
226+
Taproot get taproot => Taproot(
227+
internalKey: musig.aggregate,
228+
mast: TapLeafChecksig.apoInternal,
263229
);
264230

265231
}

coinlib/lib/src/tx/locktime.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ sealed class Locktime {
4040
? value <= blockHeight
4141
: (this as MedianTimeLocktime).time.compareTo(medianTime) <= 0;
4242

43+
/// True if this [Locktime] is the same type as the [other] locktime and comes
44+
/// before it.
45+
bool isDefinitelyBefore(Locktime other)
46+
=> value < other.value && runtimeType == other.runtimeType;
47+
4348
}
4449

4550
class BlockHeightLocktime extends Locktime {

coinlib/test/dlc/helpers.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import 'package:coinlib/coinlib.dart';
2+
import 'package:test/test.dart';
3+
import '../vectors/tx.dart';
4+
5+
final exampleLocktime = MedianTimeLocktime(DateTime(2026));
6+
final exampleOutcomeLocktime = MedianTimeLocktime(DateTime(2025, 12));
7+
8+
CETOutcome getOutcome(List<String> coins, [ Locktime? locktime ]) => CETOutcome(
9+
outputs: [
10+
for (final coinAmt in coins) Output.fromScriptBytes(
11+
CoinUnit.coin.toSats(coinAmt),
12+
exampleOutput.scriptPubKey,
13+
),
14+
],
15+
locktime: locktime ?? exampleOutcomeLocktime,
16+
network: Network.mainnet,
17+
);
18+
19+
void expectInvalidTerms(void Function() f) => expect(
20+
f, throwsA(isA<InvalidDLCTerms>()),
21+
);

coinlib/test/dlc/outcome.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'package:coinlib/coinlib.dart';
2+
import 'package:test/test.dart';
3+
import 'helpers.dart';
4+
5+
void main() {
6+
7+
setUpAll(loadCoinlib);
8+
9+
group("CETOutputs", () {
10+
11+
test(
12+
"gives totalValue",
13+
() => expect(getOutcome(["1", "5"]).totalValue, CoinUnit.coin.toSats("6")),
14+
);
15+
16+
test(
17+
"outputs cannot be empty",
18+
() => expectInvalidTerms(() => getOutcome([])),
19+
);
20+
21+
test(
22+
"outputs must reach minOutput",
23+
() => expectInvalidTerms(() => getOutcome(["0.009999"])),
24+
);
25+
26+
});
27+
28+
}

0 commit comments

Comments
 (0)