1use crate::address::{collateral_address, wallet_contract};
27use crate::build::{
28 Budget, Budgets, DRAFT_BUDGET, MAX_TX_BUDGET, check_fee, collateral_output, even, fake_signer,
29 linear_fee, reference_script_fee, settle_fee,
30};
31use crate::cbor;
32use crate::constants::{COLLATERAL_HASH, VARIANT, get_config};
33use crate::eval::{self, Resolved};
34use crate::references;
35use crate::transaction::{collateral_input, computation_fee};
36use crate::withdraw_zero::WithdrawZero;
37use anyhow::{Context, Result, anyhow, bail};
38use blstrs::{G1Projective, Scalar};
39use ff::Field;
40use group::Group;
41use hex_literal::hex;
42use pallas_addresses::{
43 Address, Network, ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart,
44};
45use pallas_codec::minicbor;
46use pallas_crypto::hash::Hash;
47use pallas_primitives::conway::{
48 ExUnits, PlutusData, ScriptRef, TransactionInput, TransactionOutput,
49};
50use pallas_primitives::{Fragment, MaybeIndefArray};
51use pallas_traverse::MultiEraTx;
52use pallas_txbuilder::{
53 BuildConway, BuiltTransaction, Input, Output, ScriptKind, StagingTransaction,
54};
55use rand_core::{OsRng, RngCore};
56use seedelf_crypto::lovejoin as crypto;
57use seedelf_crypto::register::Register;
58use seedelf_koios::koios::{ProtocolParameters, UtxoResponse};
59use uplc::tx::to_plutus_data::ToPlutusData;
60
61const UNIT: [u8; 3] = hex!("d87980");
63
64#[derive(Debug, Clone)]
66pub struct Protocol {
67 pub network_flag: bool,
68 pub denom: u64,
70 pub mix_box_hash: [u8; 28],
71 pub mix_logic_hash: [u8; 28],
72 pub references: Vec<Resolved>,
74 pub script_bytes: u64,
77}
78
79impl Protocol {
80 pub fn of(network_flag: bool) -> Result<Self> {
90 let resolved = |tx_hash: [u8; 32], output: &str| -> Result<Resolved> {
91 Ok(Resolved {
92 tx_hash,
93 index: 0,
94 output: hex::decode(output)?,
95 })
96 };
97 let (mix_box_hash, mix_logic_hash, references) = if network_flag {
98 (
99 hex!("67ffe4ed7f0ccd0a3e3069fddc26d9bccde3fe63d3d58c5e84f7ecc5"),
100 hex!("b7079c65f3b40b6da344bf68840eb0363af17787e8375004387348b8"),
101 vec![
102 resolved(
103 hex!("c5ed058606efdde7f1419c2f4b9497a2f8f3bbe4d3bb906b97cbab6c2b161190"),
104 references::LOVEJOIN_PREPROD_REFERENCE,
105 )?,
106 resolved(
107 hex!("c19c0157e236a8b01af13d7731b89d2329331588cdec3a12c5b6c599e8817a96"),
108 references::LOVEJOIN_PREPROD_MIX_BOX,
109 )?,
110 resolved(
111 hex!("06045afa3f79b25eb89c523bf9d08ef8d6605ed9c2207140cbc672669b039364"),
112 references::LOVEJOIN_PREPROD_MIX_LOGIC,
113 )?,
114 ],
115 )
116 } else {
117 (
119 hex!("c145c10ff4bcaef7f5a4dbb3fcbfddca4b6c7b08191b0690b12f1fad"),
120 hex!("0dad3046f90d9cf8354eb8b743d50fb2c25d581269b94ffcdf59499e"),
121 vec![
122 resolved(
123 hex!("f89cb43a55eed378fe90fe954a9d86566280e6416d97f9008937ab492b37175c"),
124 references::LOVEJOIN_MAINNET_REFERENCE,
125 )?,
126 resolved(
127 hex!("7d21335ffea6d3e8144ecb55f62f045f3fecd1414c932c62d7edd2886a57416a"),
128 references::LOVEJOIN_MAINNET_MIX_BOX,
129 )?,
130 resolved(
131 hex!("2452c3e6886e6d946837bfea730a76dad43aefff267b5bbffd0232a2af68c7b6"),
132 references::LOVEJOIN_MAINNET_MIX_LOGIC,
133 )?,
134 ],
135 )
136 };
137 let script_bytes = references
138 .iter()
139 .map(|r| reference_script_size(&r.output))
140 .sum::<Result<u64>>()?;
141 Ok(Protocol {
142 network_flag,
143 denom: 10_000_000,
144 mix_box_hash,
145 mix_logic_hash,
146 references,
147 script_bytes,
148 })
149 }
150
151 pub fn mix_box_address(&self) -> Address {
153 let network = if self.network_flag {
154 Network::Testnet
155 } else {
156 Network::Mainnet
157 };
158 ShelleyAddress::new(
159 network,
160 ShelleyPaymentPart::Script(Hash::new(self.mix_box_hash)),
161 ShelleyDelegationPart::Null,
162 )
163 .into()
164 }
165
166 fn reference_inputs(&self) -> Vec<Input> {
167 self.references
168 .iter()
169 .map(|r| Input::new(Hash::new(r.tx_hash), r.index))
170 .collect()
171 }
172
173 fn denom_value_bytes(&self) -> Vec<u8> {
176 let mut e = minicbor::Encoder::new(Vec::new());
177 e.map(1)
178 .and_then(|e| e.bytes(&[]))
179 .and_then(|e| e.map(1))
180 .and_then(|e| e.bytes(&[]))
181 .and_then(|e| e.u64(self.denom))
182 .expect("encoding into a Vec doesn't fail");
183 e.into_writer()
184 }
185}
186
187fn reference_script_size(output: &[u8]) -> Result<u64> {
191 let TransactionOutput::PostAlonzo(output) = minicbor::decode::<TransactionOutput>(output)
192 .map_err(|e| anyhow!("A Lovejoin reference output can't be read: {e}"))?
193 else {
194 return Ok(0);
195 };
196 Ok(match output.script_ref.map(|s| s.0) {
197 None => 0,
198 Some(ScriptRef::PlutusV1Script(s)) => s.as_ref().len() as u64,
199 Some(ScriptRef::PlutusV2Script(s)) => s.as_ref().len() as u64,
200 Some(ScriptRef::PlutusV3Script(s)) => s.as_ref().len() as u64,
201 Some(ScriptRef::NativeScript(_)) => bail!("A Lovejoin reference holds a native script"),
202 })
203}
204
205pub fn mix_datum(a: &[u8; 48], b: &[u8; 48]) -> Vec<u8> {
209 let mut e = minicbor::Encoder::new(Vec::new());
210 e.tag(minicbor::data::Tag::new(121))
211 .and_then(|e| e.begin_array())
212 .and_then(|e| e.bytes(a))
213 .and_then(|e| e.bytes(b))
214 .and_then(|e| e.end())
215 .expect("encoding into a Vec doesn't fail");
216 e.into_writer()
217}
218
219fn parse_mix_datum(datum: &[u8]) -> Option<([u8; 48], [u8; 48])> {
223 if !cbor::within_depth(datum, cbor::MAX_DEPTH) {
224 return None;
225 }
226 let PlutusData::Constr(constr) = PlutusData::decode_fragment(datum).ok()? else {
227 return None;
228 };
229 if constr.tag != 121 || constr.any_constructor.is_some() {
230 return None;
231 }
232 let [PlutusData::BoundedBytes(a), PlutusData::BoundedBytes(b)] = constr.fields.as_slice()
233 else {
234 return None;
235 };
236 let a: [u8; 48] = a.to_vec().try_into().ok()?;
237 let b: [u8; 48] = b.to_vec().try_into().ok()?;
238 (a != b).then_some((a, b))
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
243pub struct PoolBox {
244 pub utxo: Resolved,
245 pub a: [u8; 48],
246 pub b: [u8; 48],
247}
248
249impl PoolBox {
250 pub fn from_row(row: &UtxoResponse, protocol: &Protocol) -> Option<Self> {
255 if row.value != protocol.denom.to_string()
256 || row.asset_list.as_ref().is_some_and(|a| !a.is_empty())
257 || row.payment_cred != hex::encode(protocol.mix_box_hash)
258 {
259 return None;
260 }
261 let (a, b) = parse_mix_datum(&hex::decode(&row.inline_datum.as_ref()?.bytes).ok()?)?;
262 let boxed = PoolBox {
263 utxo: eval::resolve_row(row).ok()?,
264 a,
265 b,
266 };
267 boxed.points().ok().map(|_| boxed)
268 }
269
270 pub fn from_resolved(utxo: Resolved, protocol: &Protocol) -> Option<Self> {
273 let TransactionOutput::PostAlonzo(output) =
274 minicbor::decode::<TransactionOutput>(&utxo.output).ok()?
275 else {
276 return None;
277 };
278 if output.address.to_vec() != protocol.mix_box_address().to_vec()
279 || output.value != pallas_primitives::conway::Value::Coin(protocol.denom)
280 {
281 return None;
282 }
283 let Some(pallas_primitives::conway::DatumOption::Data(data)) = output.datum_option else {
284 return None;
285 };
286 let (a, b) = parse_mix_datum(&data.0.encode_fragment().ok()?)?;
287 let boxed = PoolBox { utxo, a, b };
288 boxed.points().ok().map(|_| boxed)
289 }
290
291 fn points(&self) -> Result<(G1Projective, G1Projective)> {
292 let a = crypto::point_from_bytes(&self.a)?;
293 let b = crypto::point_from_bytes(&self.b)?;
294 if bool::from(a.is_identity()) || bool::from(b.is_identity()) {
295 bail!("A box's points can't be the identity");
296 }
297 Ok((a, b))
298 }
299
300 pub fn is_owned(&self, sk: &Scalar) -> bool {
302 self.points().is_ok_and(|(a, b)| a * sk == b)
303 }
304
305 fn input(&self) -> Input {
306 Input::new(Hash::new(self.utxo.tx_hash), self.utxo.index)
307 }
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
312pub struct Coin {
313 pub utxo: Resolved,
314 pub lovelace: u64,
315}
316
317impl Coin {
318 pub fn from_row(row: &UtxoResponse) -> Result<Self> {
320 if row.asset_list.as_ref().is_some_and(|a| !a.is_empty()) {
321 bail!(
322 "UTxO {}#{} holds tokens; only ADA pays for Lovejoin",
323 row.tx_hash,
324 row.tx_index
325 );
326 }
327 Ok(Coin {
328 utxo: eval::resolve_row(row)?,
329 lovelace: row.value.parse().context("A UTxO's value isn't a number")?,
330 })
331 }
332
333 fn input(&self) -> Input {
334 Input::new(Hash::new(self.utxo.tx_hash), self.utxo.index)
335 }
336}
337
338#[derive(Debug, Clone)]
343pub struct Payer {
344 pub fee: Coin,
345 pub collateral: Option<Coin>,
347 pub address: Address,
348 pub signers: usize,
349}
350
351fn least_change(params: &ProtocolParameters, address: &Address) -> Result<u64> {
353 crate::transaction::calculate_min_required_utxo(Output::new(address.clone(), 1_000_000), params)
354}
355
356fn price(params: &ProtocolParameters, size: u64, budgets: &[Budget], script_fee: u64) -> u64 {
359 linear_fee(params, size)
360 + budgets
361 .iter()
362 .map(|b| computation_fee(params, b.mem, b.steps))
363 .sum::<u64>()
364 + script_fee
365}
366
367fn staged_units(budget: Budget) -> pallas_txbuilder::ExUnits {
369 pallas_txbuilder::ExUnits {
370 mem: budget.mem,
371 steps: budget.steps,
372 }
373}
374
375fn units(budget: Budget) -> ExUnits {
377 ExUnits {
378 mem: budget.mem,
379 steps: budget.steps,
380 }
381}
382
383fn signed_size(tx: &BuiltTransaction, signers: usize) -> Result<u64> {
384 let mut signed = tx.clone();
385 for _ in 0..signers {
386 signed = signed
387 .sign(fake_signer())
388 .context("Failed To Sign The Draft Transaction")?;
389 }
390 Ok(signed.tx_bytes.0.len() as u64)
391}
392
393#[derive(Debug, Clone)]
396pub struct Deposit {
397 pub tx: BuiltTransaction,
398 pub fee: u64,
399 pub boxes: Vec<PoolBox>,
400 pub change: Coin,
401}
402
403pub fn deposit(
407 params: &ProtocolParameters,
408 protocol: &Protocol,
409 inputs: &[Coin],
410 owners: &[Register],
411 change_address: &Address,
412 signers: usize,
413) -> Result<Deposit> {
414 if owners.is_empty() {
415 bail!("A deposit needs at least one box");
416 }
417 let datums: Vec<([u8; 48], [u8; 48])> = owners
418 .iter()
419 .map(|r| {
420 if !crate::build::is_payable(r) {
421 bail!("A box's register must be a valid, non-identity register");
422 }
423 let a: [u8; 48] = hex::decode(&r.generator)?
424 .try_into()
425 .map_err(|_| anyhow!("a bad generator"))?;
426 let b: [u8; 48] = hex::decode(&r.public_value)?
427 .try_into()
428 .map_err(|_| anyhow!("a bad public value"))?;
429 Ok((a, b))
430 })
431 .collect::<Result<_>>()?;
432 let total: u64 = inputs.iter().map(|c| c.lovelace).sum();
433 let boxed = protocol.denom * owners.len() as u64;
434 let least = least_change(params, change_address)?;
435 let (fee, staged) = settle_fee(params, signers, |fee| {
436 let change = total
437 .checked_sub(boxed + fee)
438 .filter(|c| *c >= least)
439 .context("There isn't enough ADA for these boxes, the fee and the change")?;
440 let mut tx = StagingTransaction::new();
441 for coin in inputs {
442 tx = tx.input(coin.input());
443 }
444 for (a, b) in &datums {
445 tx = tx.output(
446 Output::new(protocol.mix_box_address(), protocol.denom)
447 .set_inline_datum(mix_datum(a, b)),
448 );
449 }
450 Ok(tx
451 .output(Output::new(change_address.clone(), change))
452 .fee(fee))
453 })?;
454 let tx = staged
455 .build_conway_raw()
456 .context("Failed To Build The Deposit")?;
457 let outputs = eval::resolve_outputs(&tx.tx_bytes.0)?;
458 let boxes = datums
459 .iter()
460 .zip(&outputs)
461 .map(|((a, b), utxo)| PoolBox {
462 utxo: utxo.clone(),
463 a: *a,
464 b: *b,
465 })
466 .collect();
467 let change = Coin {
468 utxo: outputs.last().context("The deposit has no change")?.clone(),
469 lovelace: total - boxed - fee,
470 };
471 Ok(Deposit {
472 tx,
473 fee,
474 boxes,
475 change,
476 })
477}
478
479#[derive(Debug, Clone)]
481pub struct Mix {
482 pub tx: BuiltTransaction,
483 pub fee: u64,
484 pub outputs: Vec<PoolBox>,
485 pub change: Coin,
486 pub moved_to: Vec<usize>,
488}
489
490fn nonzero_scalar() -> Scalar {
492 loop {
493 let s = Scalar::random(&mut OsRng);
494 if !bool::from(s.is_zero()) {
495 return s;
496 }
497 }
498}
499
500fn permutation(n: usize) -> Vec<usize> {
502 let mut p: Vec<usize> = (0..n).collect();
503 shuffle(&mut p);
504 p
505}
506
507fn constr(index: u64, fields: Vec<PlutusData>) -> PlutusData {
509 PlutusData::Constr(pallas_primitives::Constr {
510 tag: 121 + index,
511 any_constructor: None,
512 fields: MaybeIndefArray::Indef(fields),
513 })
514}
515
516fn bytes_data(bytes: &[u8]) -> PlutusData {
517 PlutusData::BoundedBytes(bytes.to_vec().into())
518}
519
520fn list(items: Vec<PlutusData>) -> PlutusData {
521 PlutusData::Array(MaybeIndefArray::Indef(items))
522}
523
524pub fn mix(
527 params: &ProtocolParameters,
528 protocol: &Protocol,
529 boxes: &[PoolBox],
530 payer: &Payer,
531) -> Result<Mix> {
532 let n = boxes.len();
533 if n < 2 {
534 bail!("A mix needs at least two boxes");
535 }
536 let mut sorted: Vec<&PoolBox> = boxes.iter().collect();
538 sorted.sort_by_key(|b| (b.utxo.tx_hash, b.utxo.index));
539 sorted.dedup_by_key(|b| (b.utxo.tx_hash, b.utxo.index));
540 if sorted.len() != n {
541 bail!("A box can't be mixed twice in one transaction");
542 }
543
544 let perm = permutation(n);
545 let ys: Vec<Scalar> = (0..n).map(|_| nonzero_scalar()).collect();
546 let mut outputs = vec![(G1Projective::identity(), G1Projective::identity()); n];
547 for (i, boxed) in sorted.iter().enumerate() {
548 let (a, b) = boxed.points()?;
549 outputs[perm[i]] = (a * ys[i], b * ys[i]);
550 }
551 let out_bytes: Vec<([u8; 48], [u8; 48])> = outputs
552 .iter()
553 .map(|(a, b)| (crypto::point_to_bytes(a), crypto::point_to_bytes(b)))
554 .collect();
555 let datums: Vec<Vec<u8>> = out_bytes.iter().map(|(a, b)| mix_datum(a, b)).collect();
556
557 let mut preimage: Vec<u8> = datums.concat();
559 let value = protocol.denom_value_bytes();
560 for _ in 0..n {
561 preimage.extend_from_slice(&value);
562 }
563 preimage.extend_from_slice(&protocol.mix_box_hash);
564 let ctx = crypto::blake2b_256(&preimage);
565
566 let mut proofs = Vec::with_capacity(n);
567 for (i, boxed) in sorted.iter().enumerate() {
568 let (a, b) = boxed.points()?;
569 let branches = crypto::prove_sigma_or(&a, &b, &outputs, perm[i], &ys[i], &ctx)?;
570 proofs.push(constr(
571 0,
572 vec![list(
573 branches
574 .iter()
575 .map(|br| {
576 constr(
577 0,
578 vec![
579 bytes_data(&br.t0),
580 bytes_data(&br.t1),
581 bytes_data(&br.c),
582 bytes_data(&br.z),
583 ],
584 )
585 })
586 .collect(),
587 )],
588 ));
589 }
590 let redeemer = constr(1, vec![list(proofs)])
591 .encode_fragment()
592 .map_err(|e| anyhow!("{e}"))?;
593 let withdraw = WithdrawZero::new(&protocol.mix_logic_hash, protocol.network_flag, &redeemer)?;
594
595 let mut all_inputs: Vec<Input> = sorted.iter().map(|b| b.input()).collect();
596 all_inputs.push(payer.fee.input());
597 all_inputs.sort_by_key(|i| (i.tx_hash.0, i.txo_index));
598
599 let least = least_change(params, &payer.address)?;
600 let collateral = payer
603 .collateral
604 .as_ref()
605 .context("A mix needs collateral")?;
606 let stage = |fee: u64, budgets: Option<&Budgets>| -> Result<BuiltTransaction> {
607 let change = payer
608 .fee
609 .lovelace
610 .checked_sub(fee)
611 .filter(|c| *c >= least)
612 .context("There isn't enough ADA left to pay this mix's fee and keep the change")?;
613 let collateral_back = collateral
614 .lovelace
615 .checked_sub(fee * 3 / 2 + 1)
616 .context("The collateral can't cover this mix")?;
617 let mut tx = StagingTransaction::new();
618 for boxed in &sorted {
619 let index = all_inputs
620 .iter()
621 .position(|i| *i == boxed.input())
622 .expect("sorted in");
623 let budget = budgets
624 .and_then(|b| b.spend(index as u64))
625 .unwrap_or(DRAFT_BUDGET);
626 tx = tx.input(boxed.input()).add_spend_redeemer(
627 boxed.input(),
628 UNIT.to_vec(),
629 Some(staged_units(budget)),
630 );
631 }
632 tx = tx.input(payer.fee.input());
633 for (a, b) in &out_bytes {
634 tx = tx.output(
635 Output::new(protocol.mix_box_address(), protocol.denom)
636 .set_inline_datum(mix_datum(a, b)),
637 );
638 }
639 tx = tx.output(Output::new(payer.address.clone(), change));
640 for reference in protocol.reference_inputs() {
641 tx = tx.reference_input(reference);
642 }
643 tx = tx
644 .collateral_input(collateral.input())
645 .collateral_output(Output::new(payer.address.clone(), collateral_back))
646 .fee(fee)
647 .language_view(ScriptKind::PlutusV3, params.cost_model_v3.clone());
648 let built = tx.build_conway_raw().context("Failed To Build The Mix")?;
649 let reward = budgets.and_then(|b| b.withdraw(0)).unwrap_or(DRAFT_BUDGET);
650 withdraw.patch(built, units(reward), ¶ms.cost_model_v3)
651 };
652
653 let mut known: Vec<Resolved> = sorted.iter().map(|b| b.utxo.clone()).collect();
654 known.push(payer.fee.utxo.clone());
655 known.push(collateral.utxo.clone());
656 known.extend(protocol.references.iter().cloned());
657 let measure = |tx: &BuiltTransaction| -> Result<(Budgets, Vec<Budget>)> {
658 let answer = eval::evaluate(
659 &tx.tx_bytes.0,
660 &known,
661 ¶ms.cost_model_v3,
662 protocol.network_flag,
663 )?;
664 let budgets = Budgets::from_ogmios(&answer)?;
665 let mut used = Vec::with_capacity(n + 1);
666 for boxed in &sorted {
667 let index = all_inputs
668 .iter()
669 .position(|i| *i == boxed.input())
670 .expect("sorted in");
671 used.push(budgets.spend(index as u64).context("No budget for a box")?);
672 }
673 used.push(budgets.withdraw(0).context("No budget for mix_logic")?);
674 Ok((budgets, used))
675 };
676
677 let draft = stage(1_000_000, None)?;
678 let (budgets, used) = measure(&draft)?;
679 let total = used
680 .iter()
681 .fold(Budget { mem: 0, steps: 0 }, |a, b| Budget {
682 mem: a.mem + b.mem,
683 steps: a.steps + b.steps,
684 });
685 if total.mem > MAX_TX_BUDGET.mem || total.steps > MAX_TX_BUDGET.steps {
686 bail!("Mixing {n} boxes at once needs more computation than a transaction may use");
687 }
688 let script_fee = reference_script_fee(params, protocol.script_bytes)?;
689 let mut fee = 1_000_000;
690 for _ in 0..5 {
691 let tx = stage(fee, Some(&budgets))?;
692 let needed = price(params, signed_size(&tx, payer.signers)?, &used, script_fee);
693 check_fee(needed)?;
694 if needed <= fee && fee - needed < 1_000 {
695 break;
696 }
697 fee = needed;
698 }
699 let tx = stage(fee, Some(&budgets))?;
700 measure(&tx)?;
702
703 let resolved = eval::resolve_outputs(&tx.tx_bytes.0)?;
704 let outputs: Vec<PoolBox> = out_bytes
705 .iter()
706 .zip(&resolved)
707 .map(|((a, b), utxo)| PoolBox {
708 utxo: utxo.clone(),
709 a: *a,
710 b: *b,
711 })
712 .collect();
713 let change = Coin {
714 utxo: resolved.get(n).context("The mix has no change")?.clone(),
715 lovelace: payer.fee.lovelace - fee,
716 };
717 let moved_to = boxes
718 .iter()
719 .map(|b| {
720 let i = sorted
721 .iter()
722 .position(|s| s.utxo == b.utxo)
723 .expect("sorted from boxes");
724 perm[i]
725 })
726 .collect();
727 Ok(Mix {
728 tx,
729 fee,
730 outputs,
731 change,
732 moved_to,
733 })
734}
735
736#[derive(Debug, Clone)]
739pub struct Withdraw {
740 pub tx: BuiltTransaction,
741 pub fee: u64,
742 pub lovelace: u64,
743 pub register: Register,
744}
745
746pub fn owner_context(tx_cbor: &[u8], mix_box_hash: &[u8; 28]) -> Result<[u8; 32]> {
750 let tx =
751 MultiEraTx::decode(tx_cbor).map_err(|e| anyhow!("The transaction can't be read: {e}"))?;
752 let conway = tx.as_conway().context("Not a Conway transaction")?;
753 let outputs: Vec<TransactionOutput> = conway
754 .transaction_body
755 .outputs
756 .iter()
757 .map(|o| TransactionOutput::from(o.clone()))
758 .collect();
759 let mut inputs: Vec<TransactionInput> =
760 conway.transaction_body.inputs.iter().cloned().collect();
761 inputs.sort_by_key(|i| (i.transaction_id, i.index));
762 let mut preimage = uplc::plutus_data_to_bytes(&outputs.to_plutus_data());
763 preimage.extend(uplc::plutus_data_to_bytes(&inputs.to_plutus_data()));
764 preimage.extend_from_slice(mix_box_hash);
765 Ok(crypto::blake2b_256(&preimage))
766}
767
768pub fn withdraw(
772 params: &ProtocolParameters,
773 protocol: &Protocol,
774 boxes: &[PoolBox],
775 owner: &Scalar,
776 destination: Register,
777) -> Result<Withdraw> {
778 if boxes.is_empty() {
779 bail!("A withdraw needs a box");
780 }
781 if !crate::build::is_payable(&destination) {
782 bail!("The destination register must be a valid, non-identity register");
783 }
784 let mut sorted: Vec<&PoolBox> = boxes.iter().collect();
785 sorted.sort_by_key(|b| (b.utxo.tx_hash, b.utxo.index));
786 sorted.dedup_by_key(|b| (b.utxo.tx_hash, b.utxo.index));
787 if sorted.iter().any(|b| !b.is_owned(owner)) {
788 bail!("A box to withdraw isn't this wallet's");
789 }
790 let config = get_config(VARIANT, protocol.network_flag)?;
791 let to = wallet_contract(protocol.network_flag, config.contract.wallet_contract_hash);
792 let datum = destination.to_vec()?;
793 let total = protocol.denom * sorted.len() as u64;
794
795 let stage =
796 |fee: u64, budgets: Option<&Budgets>, redeemer: &[u8]| -> Result<BuiltTransaction> {
797 let lovelace = total
798 .checked_sub(fee)
799 .context("The boxes can't pay the fee")?;
800 let mut tx = StagingTransaction::new();
801 for (index, boxed) in sorted.iter().enumerate() {
802 let budget = budgets
803 .and_then(|b| b.spend(index as u64))
804 .unwrap_or(DRAFT_BUDGET);
805 tx = tx.input(boxed.input()).add_spend_redeemer(
806 boxed.input(),
807 UNIT.to_vec(),
808 Some(staged_units(budget)),
809 );
810 }
811 tx = tx.output(Output::new(to.clone(), lovelace).set_inline_datum(datum.clone()));
812 for reference in protocol.reference_inputs() {
813 tx = tx.reference_input(reference);
814 }
815 tx = tx
816 .collateral_input(collateral_input(protocol.network_flag))
817 .collateral_output(collateral_output(
818 collateral_address(protocol.network_flag),
819 fee,
820 )?)
821 .fee(fee)
822 .language_view(ScriptKind::PlutusV3, params.cost_model_v3.clone())
823 .disclosed_signer(Hash::new(COLLATERAL_HASH));
824 let built = tx
825 .build_conway_raw()
826 .context("Failed To Build The Withdraw")?;
827 let reward = budgets.and_then(|b| b.withdraw(0)).unwrap_or(DRAFT_BUDGET);
828 WithdrawZero::new(&protocol.mix_logic_hash, protocol.network_flag, redeemer)?.patch(
829 built,
830 units(reward),
831 ¶ms.cost_model_v3,
832 )
833 };
834 let prove = |tx: &BuiltTransaction| -> Result<Vec<u8>> {
836 let ctx = owner_context(&tx.tx_bytes.0, &protocol.mix_box_hash)?;
837 let mut proofs = Vec::with_capacity(sorted.len());
838 for boxed in &sorted {
839 let (a, _) = boxed.points()?;
840 let proof = crypto::prove_schnorr(&a, owner, &ctx)?;
841 proofs.push(constr(0, vec![bytes_data(&proof.t), bytes_data(&proof.z)]));
842 }
843 constr(0, vec![list(proofs)])
844 .encode_fragment()
845 .map_err(|e| anyhow!("{e}"))
846 };
847 let mut known: Vec<Resolved> = sorted.iter().map(|b| b.utxo.clone()).collect();
848 known.extend(protocol.references.iter().cloned());
849 let measure = |tx: &BuiltTransaction| -> Result<(Budgets, Vec<Budget>)> {
850 let answer = eval::evaluate(
851 &tx.tx_bytes.0,
852 &known,
853 ¶ms.cost_model_v3,
854 protocol.network_flag,
855 )?;
856 let budgets = Budgets::from_ogmios(&answer)?;
857 let mut used = Vec::with_capacity(sorted.len() + 1);
858 for index in 0..sorted.len() as u64 {
859 used.push(budgets.spend(index).context("No budget for a box")?);
860 }
861 used.push(budgets.withdraw(0).context("No budget for mix_logic")?);
862 Ok((budgets, used))
863 };
864 let placeholder = {
868 let one = constr(0, vec![bytes_data(&sorted[0].a), bytes_data(&[0u8; 32])]);
869 constr(0, vec![list(vec![one; sorted.len()])])
870 .encode_fragment()
871 .map_err(|e| anyhow!("{e}"))?
872 };
873
874 let script_fee = reference_script_fee(params, protocol.script_bytes)?;
878 let mut fee = 500_000;
879 let mut declared: Option<Budgets> = None;
880 for _ in 0..8 {
881 let unproven = stage(fee, declared.as_ref(), &placeholder)?;
882 let redeemer = prove(&unproven)?;
883 let tx = stage(fee, declared.as_ref(), &redeemer)?;
884 let (measured, _) = measure(&tx)?;
885 match &declared {
886 Some(d) if d.covers(&measured) => {
887 let mut charged = Vec::with_capacity(sorted.len() + 1);
888 for index in 0..sorted.len() as u64 {
889 charged.push(d.spend(index).context("No budget for a box")?);
890 }
891 charged.push(d.withdraw(0).context("No budget for mix_logic")?);
892 let needed = price(params, signed_size(&tx, 1)?, &charged, script_fee);
893 if needed <= fee && fee - needed < 5_000 {
894 return Ok(Withdraw {
895 tx,
896 fee,
897 lovelace: total - fee,
898 register: destination,
899 });
900 }
901 fee = even(needed);
905 }
906 _ => declared = Some(measured.with_margin(1)),
907 }
908 }
909 bail!("The withdraw's fee did not settle")
910}
911
912pub fn mixes_per_box(depth: u32) -> usize {
916 (3usize.pow(depth) - 1) / 2
917}
918
919pub const MIX_FEE_ESTIMATE: u64 = 950_000;
925
926const DEPOSIT_RESERVE: u64 = 1_500_000;
929
930fn per_box(depth: u32, denom: u64) -> u64 {
932 denom + mixes_per_box(depth) as u64 * MIX_FEE_ESTIMATE
933}
934
935fn per_box_again(depth: u32) -> u64 {
938 mixes_per_box(depth) as u64 * MIX_FEE_ESTIMATE
939}
940
941pub fn boxes_affordable(spare: u64, depth: u32, denom: u64) -> usize {
944 (spare.saturating_sub(DEPOSIT_RESERVE) / per_box(depth, denom)) as usize
945}
946
947pub fn funding_for(boxes: usize, depth: u32, denom: u64) -> u64 {
951 DEPOSIT_RESERVE + boxes as u64 * per_box(depth, denom)
952}
953
954pub fn again_affordable(spare: u64, depth: u32) -> usize {
957 (spare.saturating_sub(DEPOSIT_RESERVE) / per_box_again(depth)) as usize
958}
959
960pub fn again_funding(boxes: usize, depth: u32) -> u64 {
964 DEPOSIT_RESERVE + boxes as u64 * per_box_again(depth)
965}
966
967#[derive(Debug, Clone)]
972pub struct Funding {
973 pub coins: Vec<Coin>,
974 pub collateral: Option<Coin>,
977 pub address: Address,
978 pub deposit_signers: usize,
979 pub mix_signers: usize,
980}
981
982#[derive(Debug, Clone)]
984pub struct ChainTx {
985 pub kind: &'static str,
987 pub tx: BuiltTransaction,
988 pub fee: u64,
989}
990
991#[derive(Debug, Clone)]
994pub struct Chain {
995 pub txs: Vec<ChainTx>,
996 pub leaves: Vec<PoolBox>,
997 pub change: Coin,
998}
999
1000pub fn chain(
1007 params: &ProtocolParameters,
1008 protocol: &Protocol,
1009 funding: &Funding,
1010 owners: &[Register],
1011 depth: u32,
1012 pool: &[PoolBox],
1013) -> Result<Chain> {
1014 let Funding {
1015 coins,
1016 collateral,
1017 address,
1018 deposit_signers,
1019 mix_signers,
1020 } = funding;
1021 let fresh = fresh_for(owners.len(), depth, pool, &[])?;
1022 let deposit = deposit(params, protocol, coins, owners, address, *deposit_signers)?;
1023 if depth > 0 && collateral.is_none() {
1025 bail!("A chain that mixes needs collateral");
1026 }
1027 let payer = Payer {
1028 fee: deposit.change.clone(),
1029 collateral: collateral.clone(),
1030 address: address.clone(),
1031 signers: *mix_signers,
1032 };
1033 let mut fanned = fan_out(params, protocol, deposit.boxes, &payer, depth, fresh)?;
1034 fanned.txs.insert(
1035 0,
1036 ChainTx {
1037 kind: "deposit",
1038 tx: deposit.tx,
1039 fee: deposit.fee,
1040 },
1041 );
1042 Ok(fanned)
1043}
1044
1045pub fn again(
1052 params: &ProtocolParameters,
1053 protocol: &Protocol,
1054 payer: &Payer,
1055 boxes: &[PoolBox],
1056 depth: u32,
1057 pool: &[PoolBox],
1058) -> Result<Chain> {
1059 if boxes.is_empty() {
1060 bail!("There's no box to mix again");
1061 }
1062 let fresh = fresh_for(boxes.len(), depth, pool, boxes)?;
1063 fan_out(params, protocol, boxes.to_vec(), payer, depth, fresh)
1064}
1065
1066#[derive(Debug, Clone, PartialEq, Eq)]
1070pub struct PoolShort {
1071 pub have: usize,
1072 pub needed: usize,
1073}
1074
1075impl std::fmt::Display for PoolShort {
1076 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1077 write!(
1078 f,
1079 "Lovejoin's pool has {} boxes to mix with, and this needs {}",
1080 self.have, self.needed
1081 )
1082 }
1083}
1084
1085impl std::error::Error for PoolShort {}
1086
1087fn fresh_for(trees: usize, depth: u32, pool: &[PoolBox], ours: &[PoolBox]) -> Result<Vec<PoolBox>> {
1091 if !(0..=3).contains(&depth) {
1096 bail!("The fan-out is 0 to 3 waves deep");
1097 }
1098 let mut fresh: Vec<PoolBox> = pool
1099 .iter()
1100 .filter(|b| !ours.iter().any(|o| o.utxo == b.utxo))
1101 .cloned()
1102 .collect();
1103 let needed = trees * mixes_per_box(depth) * 2;
1104 if fresh.len() < needed {
1105 return Err(PoolShort {
1106 have: fresh.len(),
1107 needed,
1108 }
1109 .into());
1110 }
1111 shuffle(&mut fresh);
1112 Ok(fresh)
1113}
1114
1115fn fan_out(
1120 params: &ProtocolParameters,
1121 protocol: &Protocol,
1122 start: Vec<PoolBox>,
1123 payer: &Payer,
1124 depth: u32,
1125 mut fresh: Vec<PoolBox>,
1126) -> Result<Chain> {
1127 let mut txs = Vec::new();
1128 let mut change = payer.fee.clone();
1129 let mut ours: Vec<PoolBox> = start.clone();
1130 let mut wave: Vec<PoolBox> = start;
1132 for _ in 0..depth {
1133 shuffle(&mut wave);
1134 let mut next = Vec::with_capacity(wave.len() * 3);
1135 let mut ours_next = Vec::new();
1136 for boxed in &wave {
1137 let others = [
1138 fresh.pop().expect("counted by fresh_for"),
1139 fresh.pop().expect("counted by fresh_for"),
1140 ];
1141 let inputs = vec![boxed.clone(), others[0].clone(), others[1].clone()];
1142 let paying = Payer {
1143 fee: change.clone(),
1144 ..payer.clone()
1145 };
1146 let mixed = mix(params, protocol, &inputs, &paying)?;
1147 if ours.contains(boxed) {
1148 ours_next.push(mixed.outputs[mixed.moved_to[0]].clone());
1149 }
1150 change = mixed.change.clone();
1151 next.extend(mixed.outputs.iter().cloned());
1152 txs.push(ChainTx {
1153 kind: "mix",
1154 tx: mixed.tx,
1155 fee: mixed.fee,
1156 });
1157 }
1158 ours = ours_next;
1159 wave = next;
1160 }
1161 Ok(Chain {
1162 txs,
1163 leaves: ours,
1164 change,
1165 })
1166}
1167
1168pub(crate) fn shuffle<T>(items: &mut [T]) {
1171 for i in (1..items.len()).rev() {
1172 let j = (OsRng.next_u64() % (i as u64 + 1)) as usize;
1173 items.swap(i, j);
1174 }
1175}