-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathbasic.rs
More file actions
567 lines (491 loc) · 24.1 KB
/
basic.rs
File metadata and controls
567 lines (491 loc) · 24.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
use dlctix::bitcoin;
use dlctix::convert_xonly_key;
use dlctix::musig2;
use dlctix::secp::{MaybePoint, Point, Scalar};
use dlctix::{
hashlock, ContractParameters, ContributorPartialSignatureSharingRound, EventLockingConditions,
MarketMaker, NonceSharingRound, Outcome, PayoutWeights, Player, SigMap, SignedContract,
SigningSession, TicketedDLC, WinCondition,
};
use std::collections::BTreeMap;
/*
This demo illustrates the use of dlctix to create a basic two-party ticketed DLC,
and then enforce different outcomes.
*/
#[test]
fn two_player_example() -> Result<(), Box<dyn std::error::Error>> {
let mut rng = rand::rng();
// Define the players' secret data. Each player would normally generate
// and store their own secret key and payout preimage on their own machine.
let alice_seckey = Scalar::random(&mut rng);
let alice_payout_preimage = hashlock::preimage_random(&mut rng);
let bob_seckey = Scalar::random(&mut rng);
let bob_payout_preimage = hashlock::preimage_random(&mut rng);
// The market maker generates a ticket preimage (secret) for each player.
// If a player learns their preimage, they can enforce winning outcomes favorable
// to them. So the market maker should keep these secret, and only give a player
// their ticket preimage if they pay for it appropriately first.
let alice_ticket_preimage = hashlock::preimage_random(&mut rng);
let bob_ticket_preimage = hashlock::preimage_random(&mut rng);
// The market maker has his own key pair as well.
let market_maker_seckey = Scalar::random(&mut rng);
let market_maker_pubkey = market_maker_seckey.base_point_mul();
// This is public data which the market maker shares with all players.
let alice = Player {
pubkey: alice_seckey.base_point_mul(),
ticket_hash: hashlock::sha256(&alice_ticket_preimage),
payout_hash: hashlock::sha256(&alice_payout_preimage),
};
let bob = Player {
pubkey: bob_seckey.base_point_mul(),
ticket_hash: hashlock::sha256(&bob_ticket_preimage),
payout_hash: hashlock::sha256(&bob_payout_preimage),
};
let players = vec![
alice.clone(), // Alice has player index 0
bob.clone(), // Bob has player index 1
];
// To execute any DLC, there must be a semi-trusted oracle who
// attests to the outcome. The oracle has the power to dictate the
// outcome of the contract, but doesn't need to be directly involved.
// Oracles usually publish their announcements and attestations over
// public mediums like a website, or Twitter, or Nostr.
let oracle_seckey = Scalar::random(&mut rng);
let oracle_pubkey = oracle_seckey.base_point_mul();
// Each event has an associated nonce which the oracle commits to
// ahead of time.
let oracle_secnonce = Scalar::random(&mut rng);
let nonce_point = oracle_secnonce.base_point_mul();
// We enumerate the different outcome messages the oracle could sign...
let outcome_messages = vec![
Vec::from(b"alice wins"),
Vec::from(b"bob wins"),
Vec::from(b"tie"),
];
// ...and then precompute the locking points needed for each possible outcome.
let locking_points: Vec<MaybePoint> = outcome_messages
.iter()
.map(|msg| dlctix::attestation_locking_point(oracle_pubkey, nonce_point, msg))
.collect();
// This struct describes the different possible outcomes an oracle might sign.
let event = EventLockingConditions {
locking_points,
// The expiry time is the time after which the Expiry outcome transaction should be
// triggered. This can either be a unix seconds timestamp, or a bitcoin block height,
// or `None` to indicate the contract should not expire.
expiry: Some(1710963648),
};
// A set of PayoutWeights describes who is paid out and how much is allocated to each
// winner. The keys are player indexes (e.g. 0 for Alice, 1 for Bob), and the values
// are relative weights.
//
// For example, `PayoutWeights::from([(0, 1), (1, 2)])` allocates two thirds of the pot
// to player 1, and one third to player 0.
//
// If there is only one winner in the PayoutWeights map, then they are always allocated
// the full pot. Payout weight values cannot be zero, or DLC TX construction will fail.
let alice_wins_payout = PayoutWeights::from([(0, 1)]); // all to Alice
let bob_wins_payout = PayoutWeights::from([(1, 1)]); // all to Bob
let tie_payout = PayoutWeights::from([(0, 1), (1, 1)]); // split the pot evenly
// An Outcome is a compact representation of which of the messages in the
// `EventLockingConditions::outcome_messages` field (if any) an oracle might attest
// to.
let alice_wins_outcome = Outcome::Attestation(0);
let bob_wins_outcome = Outcome::Attestation(1);
let tie_outcome = Outcome::Attestation(2);
// The outcome payouts map describes how payouts are allocated based on the Outcome
// which should be attested to by the oracle. If the oracle doesn't attest to any
// outcome by the expiry time, then the `Outcome::Expiry` payout map will take effect.
// If this map does not contain an `Outcome::Expiry` entry, then there is no expiry
// condition, and the money simply remains locked in the funding output until the
// Oracle's attestation is found.
let outcome_payouts = BTreeMap::from([
(alice_wins_outcome, alice_wins_payout.clone()),
(bob_wins_outcome, bob_wins_payout),
(tie_outcome, tie_payout.clone()),
(Outcome::Expiry, tie_payout.clone()),
]);
// We are finally ready to construct the full set of contract parameters.
let params = ContractParameters {
market_maker: MarketMaker {
pubkey: market_maker_pubkey,
},
players,
event: event.clone(),
outcome_payouts,
// This determines a flat fee rate used for each cooperatively-signed transaction.
// Ideally it should be high enough to cover unexpected surges in the fee market,
// Callers may also wish to consider signing multiple sets of Ticketed DLC transactions
// under different fee rates.
fee_rate: bitcoin::FeeRate::from_sat_per_vb_unchecked(100),
// This determines the amount of bitcoin which the market maker is expected to use
// to fund the contract on-chain. Normally, this would be the expected sum of the
// players' off-chain payments to the market maker, minus a fee. Winners will split
// the funding value among themselves according to the agreed PayoutWeights for
// each outcome.
funding_value: bitcoin::Amount::from_sat(1_000_000),
// A reasonable number of blocks within which a transaction can confirm.
// Used for enforcing relative locktime timeout spending conditions.
//
// Reasonable values are:
//
// - `72`: ~12 hours
// - `144`: ~24 hours
// - `432`: ~72 hours
// - `1008`: ~1 week
relative_locktime_block_delta: 72,
};
// Usually the market maker would construct the ContractParameters, and would send it
// to all players. The players can validate it to ensure it meets their expectations, and
// that they are paid out in the correct situations. Here are a few examples of things
// Alice might validate.
{
// Ensure Alice is a player in the DLC. This ensures her signatures are needed
// to unlock the funding output.
assert_eq!(params.players[0], alice);
// Ensure the market maker is funding the right amount.
assert_eq!(params.funding_value, bitcoin::Amount::from_sat(1_000_000));
// Alice should be paid out in full if she wins.
assert_eq!(
params.outcome_payouts[&alice_wins_outcome],
alice_wins_payout
);
// Alice should be paid out half if there is a tie or expiry.
assert_eq!(params.outcome_payouts[&tie_outcome], tie_payout);
assert_eq!(params.outcome_payouts[&Outcome::Expiry], tie_payout);
// Also do basic safety checks to ensure the market maker isn't fiddling
// with the relative locktime
assert_eq!(params.fee_rate.to_sat_per_vb_floor(), 100);
// ...or using a crazy fee rate.
assert_eq!(params.relative_locktime_block_delta, 72);
// ...or modifying the expected oracle event.
assert_eq!(params.event, event);
}
// Alice is now confident that her contract parameters match her expectations,
// and if the market maker funds the contract, she can participate without
// trusting anyone but the oracle.
let funding_output = params.funding_output()?;
let mut funding_tx = bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![], // the market maker would handle funding
output: vec![funding_output],
};
// The market maker shouldn't broadcast the funding_tx yet, because he
// first needs players' signatures to ensure he can recover his money
// if they disappear.
let funding_outpoint = bitcoin::OutPoint {
txid: funding_tx.compute_txid(),
vout: 0,
};
// Every player and the market maker uses the same set of ContractParameters and
// the market maker's funding outpoint to construct a `TicketedDLC`, which
// encapsulates all the unsigned transactions.
let ticketed_dlc = TicketedDLC::new(params, funding_outpoint)?;
// All participants now run a cooperative MuSig signing session to sign all the
// various transactions. This usually consists of two rounds of network communication.
//
// - the players submit nonces to the market maker
// - the market maker aggregates the nonces and replies with a set of aggregated nonces
// - the players use the aggregated nonces to compute and submit sets of partial signatures
// - the market maker validates and aggregates the partial signatures into a fully SignedContract
//
// For more details, see the `musig_sign_ticketed_dlc` function below, where we simulate
// the process locally.
let signed_contract: SignedContract = musig_sign_ticketed_dlc(
&ticketed_dlc,
[alice_seckey, bob_seckey, market_maker_seckey],
&mut rng,
);
// The `signed_contract` can now be used to enforce DLC outcomes, but the contract hasn't been
// funded yet. An optimistic market maker might immediately fund the contract, but they could
// also require players to forward a small anti-spam deposit to the market maker, to offset
// the risk that a player might renege and opt out of buying their ticket preimage.
//
// Once the market maker is ready, they can broadcast the funding TX to lock in the Ticketed DLC.
sign_transaction(&mut funding_tx);
broadcast_transaction(&funding_tx);
wait_for_confs(&funding_tx.compute_txid(), 1);
// Once the funding TX is confirmed, players can begin buying their ticket preimages
// from the market maker. This would probably take place via the lightning network,
// outside the scope of this crate.
//
// Once Alice has her ticket preimage, she can now confidently enforce any outcome in
// which her player index is part of the PayoutWeights map.
//
// However, before _any_ outcome can be enforced, the oracle must publish their attestation.
let outcome_index = 0;
let oracle_attestation = dlctix::attestation_secret(
oracle_seckey,
oracle_secnonce,
&outcome_messages[outcome_index],
);
// A win condition describes an outcome and a particular player
// who is paid out under that outcome.
let alice_win_cond = WinCondition {
outcome: Outcome::Attestation(outcome_index),
player_index: 0,
};
// At this stage, Alice knows her ticket preimage, and so she is 100% confident she'll
// be able to claim her winnings. Here's how.
let claim_winnings_forcefully = || -> Result<(), Box<dyn std::error::Error>> {
// An outcome transaction spends the funding outpoint, and locks it into
// a 2nd stage multisig contract between the outcome winners and the market maker.
// If Alice (or any other player) knows the attestation to outcome 0, she can
// unlock that outcome TX and publish it.
let outcome_tx = signed_contract.signed_outcome_tx(outcome_index, oracle_attestation)?;
broadcast_transaction(&outcome_tx);
// Alice must wait for the relative locktime to expire before she can use the split transaction.
wait_for_confs(
&outcome_tx.compute_txid(),
signed_contract.params().relative_locktime_block_delta,
);
let split_tx = signed_contract.signed_split_tx(&alice_win_cond, alice_ticket_preimage)?;
broadcast_transaction(&split_tx);
// Alice must wait for the relative locktime to expire before she can extract her money
// from the split transaction output.
wait_for_confs(
&split_tx.compute_txid(),
signed_contract.params().relative_locktime_block_delta,
);
// This prevout data is needed to construct the signature and
// also is helpful in constructing transactions which aggregate
// multiple inputs. Perhaps Alice might want to join the
// winnings together with other coins.
let (alice_split_input, alice_split_prevout) = signed_contract
.split_win_tx_input_and_prevout(&alice_win_cond)
.unwrap();
let mut alice_win_tx = simple_sweep_tx(
alice.pubkey,
alice_split_input,
signed_contract.split_win_tx_input_weight(),
alice_split_prevout.value,
);
signed_contract.sign_split_win_tx_input(
&alice_win_cond,
&mut alice_win_tx,
0, // input index
&bitcoin::sighash::Prevouts::All(&[alice_split_prevout]),
alice_ticket_preimage,
alice_seckey,
)?;
broadcast_transaction(&alice_win_tx);
wait_for_confs(&alice_win_tx.compute_txid(), 1);
// Alice now has 100% control over her winnings.
Ok(())
};
claim_winnings_forcefully()?;
// However, forceful resolution is far less efficient than cooperating. To
// streamline the resolution process, Alice gives the market maker a lightning
// invoice, which pays Alice out if she reveals `alice_payout_preimage`. Alice's
// payout preimage gives the market maker the ability to reclaim the winnings Alice
// could've claimed. Here's how.
let reclaim_winnings_via_split_sellback = || -> Result<(), Box<dyn std::error::Error>> {
let outcome_tx = signed_contract.signed_outcome_tx(outcome_index, oracle_attestation)?;
broadcast_transaction(&outcome_tx);
let (alice_split_input, alice_split_prevout) =
signed_contract.split_sellback_tx_input_and_prevout(&alice_win_cond)?;
let mut sellback_tx = simple_sweep_tx(
market_maker_pubkey,
alice_split_input,
signed_contract.split_sellback_tx_input_weight(),
alice_split_prevout.value,
);
signed_contract.sign_split_sellback_tx_input(
&alice_win_cond,
&mut sellback_tx,
0, // input index
&bitcoin::sighash::Prevouts::All(&[alice_split_prevout]),
alice_payout_preimage,
market_maker_seckey,
)?;
Ok(())
};
reclaim_winnings_via_split_sellback()?;
// But this can be improved even more. Once Alice has been paid off-chain, and
// the market maker has her payout preimage, her secret key no longer has any
// value. She can freely surrender it to the market maker without any negative
// effects. This allows the market maker to sweep the output of the outcome
// TX without using inefficient HTLC script logic. Here's how.
let reclaim_winnings_via_outcome_close = || -> Result<(), Box<dyn std::error::Error>> {
// TODO
Ok(())
};
reclaim_winnings_via_outcome_close()?;
// Bob lost, so he has no incentive to participate in the protocol at all once the
// attestation is revealed
//
// But on the off chance Bob is cooperative as well, and if the maker knows Alice's
// payout preimage, then Alice and Bob can both surrender their secret keys. The market
// maker can use them to sweep the funding output right back to themselves, resulting
// in the most efficient and private on-chain footprint possible for the contract.
let reclaim_winnings_via_funding_close = || -> Result<(), Box<dyn std::error::Error>> {
let (close_tx_input, close_tx_prevout) =
signed_contract.funding_close_tx_input_and_prevout();
let mut close_tx = simple_sweep_tx(
market_maker_pubkey,
close_tx_input,
signed_contract.close_tx_input_weight(),
close_tx_prevout.value,
);
signed_contract.sign_funding_close_tx_input(
&mut close_tx,
0, // input index
&bitcoin::sighash::Prevouts::All(&[close_tx_prevout]),
market_maker_seckey,
&BTreeMap::from([(alice.pubkey, alice_seckey), (bob.pubkey, bob_seckey)]),
)?;
Ok(())
};
reclaim_winnings_via_funding_close()?;
// If Alice didn't buy her ticket preimage, Alice can still unlock the outcome transaction,
// but she can't unlock the split transaction. Eventually, after a locktime delay, the
// outcome TX output can be reclaimed by the market maker.
let reclaim_winnings_via_timeout = || -> Result<(), Box<dyn std::error::Error>> {
let outcome = Outcome::Attestation(outcome_index);
let outcome_tx = signed_contract.signed_outcome_tx(outcome_index, oracle_attestation)?;
broadcast_transaction(&outcome_tx);
// The reclaim TX spending path is only unlocked after double the locktime
// needed for the split TX.
wait_for_confs(
&outcome_tx.compute_txid(),
2 * signed_contract.params().relative_locktime_block_delta,
);
let (reclaim_tx_input, reclaim_tx_prevout) =
signed_contract.outcome_reclaim_tx_input_and_prevout(&outcome)?;
let mut reclaim_tx = simple_sweep_tx(
market_maker_pubkey,
reclaim_tx_input,
signed_contract
.outcome_reclaim_tx_input_weight(&outcome)
.unwrap(),
reclaim_tx_prevout.value,
);
signed_contract.sign_outcome_reclaim_tx_input(
&outcome,
&mut reclaim_tx,
0, // input index
&bitcoin::sighash::Prevouts::All(&[reclaim_tx_prevout]),
market_maker_seckey,
)?;
Ok(())
};
reclaim_winnings_via_timeout()?;
Ok(())
}
/// Cooperatively sign a `TicketedDLC` using the secret keys of every player
/// and the market maker. The order of secret keys in the `all_seckeys` iterator
/// does not matter.
fn musig_sign_ticketed_dlc<R: rand::RngCore + rand::CryptoRng>(
ticketed_dlc: &TicketedDLC,
all_seckeys: impl IntoIterator<Item = Scalar>,
rng: &mut R,
) -> SignedContract {
let mut signing_sessions: BTreeMap<Point, SigningSession<NonceSharingRound>> = all_seckeys
.into_iter()
.map(|seckey| {
let session = SigningSession::new(ticketed_dlc.clone(), rng, seckey)
.expect("error creating SigningSession");
(seckey.base_point_mul(), session)
})
.collect();
let pubnonces_by_sender: BTreeMap<Point, SigMap<musig2::PubNonce>> = signing_sessions
.iter()
.map(|(&sender_pubkey, session)| {
// Simulate serialization, as pubnonces are usually sent over a transport channel.
let serialized_nonces = serde_json::to_string(session.our_public_nonces())
.expect("error serializing pubnonces");
let received_pubnonces =
serde_json::from_str(&serialized_nonces).expect("error deserializing pubnonces");
(sender_pubkey, received_pubnonces)
})
.collect();
let coordinator_session = signing_sessions
.remove(&ticketed_dlc.params().market_maker.pubkey)
.unwrap()
.aggregate_nonces_and_compute_partial_signatures(pubnonces_by_sender)
.expect("error aggregating pubnonces");
let signing_sessions: BTreeMap<Point, SigningSession<ContributorPartialSignatureSharingRound>> =
signing_sessions
.into_iter()
.map(|(pubkey, session)| {
let new_session = session
.compute_partial_signatures(coordinator_session.aggregated_nonces().clone())
.expect("failed to compute partial signatures");
(pubkey, new_session)
})
.collect();
let partial_sigs_by_sender: BTreeMap<Point, SigMap<musig2::PartialSignature>> =
signing_sessions
.iter()
.map(|(&sender_pubkey, session)| {
let serialized_sigs = serde_json::to_string(session.our_partial_signatures())
.expect("error serializing partial signatures");
let received_sigs = serde_json::from_str(&serialized_sigs)
.expect("error deserializing partial signatures");
(sender_pubkey, received_sigs)
})
.collect();
// Every player's signatures can be verified individually by the coordinator.
for (&sender_pubkey, partial_sigs) in &partial_sigs_by_sender {
coordinator_session
.verify_partial_signatures(sender_pubkey, partial_sigs)
.expect("valid partial signatures should be verified as OK");
}
let signed_contract = coordinator_session
.aggregate_all_signatures(partial_sigs_by_sender)
.expect("error aggregating partial signatures");
for session in signing_sessions.into_values() {
session
.verify_aggregated_signatures(signed_contract.all_signatures())
.expect("player failed to verify signatures aggregated by the market maker");
// This is how a player receiving signatures from the market maker might convert
// their signing session into a complete SignedContract.
let _: SignedContract =
session.into_signed_contract(signed_contract.all_signatures().clone());
}
// SignedContract should be able to be stored and retrieved via serde serialization.
let decoded_contract = serde_json::from_str(
&serde_json::to_string(&signed_contract).expect("error serializing SignedContract"),
)
.expect("error deserializing SignedContract");
assert_eq!(
signed_contract, decoded_contract,
"deserialized SignedContract does not match original"
);
signed_contract
}
/// Used for demonstration
fn sign_transaction(_: &mut bitcoin::Transaction) {}
fn broadcast_transaction(_: &bitcoin::Transaction) {}
fn wait_for_confs(_: &bitcoin::Txid, _: u16) {}
/// Generate a P2TR script pubkey which pays to the given pubkey (no tweak added).
fn p2tr_script_pubkey(pubkey: Point) -> bitcoin::ScriptBuf {
let (xonly, _) = pubkey.into();
let tweaked =
bitcoin::key::TweakedPublicKey::dangerous_assume_tweaked(convert_xonly_key(xonly));
bitcoin::ScriptBuf::new_p2tr_tweaked(tweaked)
}
/// Create a simple TX which sweeps to the given destination pubkey as a P2TR output.
fn simple_sweep_tx(
destination_pubkey: Point,
input: bitcoin::TxIn,
input_weight: bitcoin::transaction::InputWeightPrediction,
prevout_value: bitcoin::Amount,
) -> bitcoin::Transaction {
let script_pubkey = p2tr_script_pubkey(destination_pubkey);
bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![input],
output: vec![bitcoin::TxOut {
value: {
let tx_weight =
bitcoin::transaction::predict_weight([input_weight], [script_pubkey.len()]);
let fee = tx_weight * bitcoin::FeeRate::from_sat_per_vb_unchecked(20);
prevout_value - fee
},
script_pubkey,
}],
}
}