Skip to main content

seedelf_core/
lovejoin.rs

1//! Lovejoin, the mixer (<https://github.com/logical-mechanism/Lovejoin>): its
2//! deployments on preprod and mainnet, the pool's boxes, and the three
3//! transactions the wallet makes with it.
4//!
5//! - **Deposit:** a key account locks 10 ₳ boxes at `mix_box`, each with an
6//!   inline datum `{a, b}`. Nothing validates a deposit. The wallet's boxes are
7//!   a Seedelf register's shape (`b = [x]·a`), owned by the Seedelf key.
8//! - **Mix:** N boxes in, N re-randomized boxes out, in a random order, at
9//!   outputs 0..N−1, with one sigma-OR proof per box in `mix_logic`'s
10//!   withdraw-zero redeemer. A key account pays the fee and puts up the
11//!   collateral: no fee shard, so the on-chain floor is N ≥ 2. The proofs bind
12//!   only the box outputs, so the change is priced after the proofs are made.
13//! - **Withdraw:** boxes into one fresh Seedelf register, paid from the boxes,
14//!   with giveme.my's collateral and no key of ours on it. One Schnorr proof
15//!   per box binds every output and input, so the fee is settled by building,
16//!   proving and measuring until it stops moving.
17//!
18//! A [`chain`] is a deposit and each of its boxes fanned out; [`again`] fans
19//! out boxes of ours already in the pool, with no deposit.
20//!
21//! Every transaction here is measured in the wallet ([`crate::eval`]) against
22//! the deployed scripts, bundled with their reference UTxOs
23//! ([`crate::references`]), so a chain of them can be built before any is on
24//! chain.
25
26use 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
61/// The unit datum, `Constr 0 []`: `mix_box`'s spend redeemer (it reads none).
62const UNIT: [u8; 3] = hex!("d87980");
63
64/// Lovejoin's deployment on a network, with what evaluating against it needs.
65#[derive(Debug, Clone)]
66pub struct Protocol {
67    pub network_flag: bool,
68    /// Every box holds exactly this, and only ADA (`denom_lovelace`).
69    pub denom: u64,
70    pub mix_box_hash: [u8; 28],
71    pub mix_logic_hash: [u8; 28],
72    /// The protocol's reference UTxO and the two scripts' reference UTxOs.
73    pub references: Vec<Resolved>,
74    /// The reference scripts' size, for the reference-script fee, read from
75    /// `references`.
76    pub script_bytes: u64,
77}
78
79impl Protocol {
80    /// Lovejoin on preprod (`true`, as everywhere in the workspace) or on
81    /// mainnet (`false`, live since 2026-09-26). Mainnet's scripts are
82    /// preprod's logic recompiled, so their hashes and sizes differ.
83    ///
84    /// The reference scripts' size, which prices every mix and withdraw, is
85    /// read from the bundled reference outputs, never typed in: mainnet's
86    /// `mix_logic` is 3,161 bytes to preprod's 3,156, and preprod's number
87    /// would have priced every mainnet mix 75 lovelace under the ledger's
88    /// minimum.
89    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            // Lovejoin cb5a5a3, artifacts/mainnet/addresses.json.
118            (
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    /// `mix_box`'s enterprise script address, where every box sits.
152    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    /// `serialise_data` of a box's value, `{"": {"": denom}}`, as the mix
174    /// context takes it.
175    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
187/// The size of the script a reference output carries, as the ledger counts
188/// it for the reference-script fee: the script's bytes inside its
189/// `script_ref`. 0 for an output with none.
190fn 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
205/// A box's datum, `Constr 0 [a, b]`, in the canonical form `serialise_data`
206/// gives it (fields as an indefinite list), so what the mix context hashes is
207/// byte for byte what the transaction holds.
208pub 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
219/// `{a, b}` from a box's datum, if it's the well-formed shape. Anyone can pay
220/// `mix_box` any datum, so one nested too deeply to decode safely is skipped
221/// unread.
222fn 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/// A box in the pool: where it is and its `{a, b}`.
242#[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    /// A pool row, if it's a box a mix may take: at `mix_box`, exactly the
251    /// denomination in ADA alone, an inline `{a, b}` with `a ≠ b`, both points
252    /// in G1's prime-order subgroup and neither the identity. Anything else at
253    /// the address is someone's mistake, left alone.
254    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    /// A UTxO this wallet knows the bytes of (a chain's own output, say), if
271    /// it's a box a mix may take, by the same rules as [`Self::from_row`].
272    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    /// Whether `sk` owns the box: `b = [sk]·a`, the Seedelf register check.
301    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/// A key account's ADA-only UTxO: a fee input, a collateral, or change.
311#[derive(Debug, Clone, PartialEq, Eq)]
312pub struct Coin {
313    pub utxo: Resolved,
314    pub lovelace: u64,
315}
316
317impl Coin {
318    /// A Koios row, if it holds ADA alone.
319    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/// Who pays a deposit or a mix: its fee comes from `fee`, its collateral is
339/// `collateral` (left unspent), and the change goes back to `address`.
340/// `signers` is how many keys sign it (the fee's and the collateral's): a
341/// session's one key, or two of a public account's.
342#[derive(Debug, Clone)]
343pub struct Payer {
344    pub fee: Coin,
345    /// None only where nothing is mixed: [`mix`] refuses without it.
346    pub collateral: Option<Coin>,
347    pub address: Address,
348    pub signers: usize,
349}
350
351/// The least an ADA-only output at `address` may hold.
352fn 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
356/// A script transaction's fee: its signed `size`, the `budgets` it declares,
357/// and `script_fee`, its reference scripts' ([`reference_script_fee`]).
358fn 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
367/// A budget as a staged redeemer carries it.
368fn staged_units(budget: Budget) -> pallas_txbuilder::ExUnits {
369    pallas_txbuilder::ExUnits {
370        mem: budget.mem,
371        steps: budget.steps,
372    }
373}
374
375/// A budget as the withdraw-zero patch writes it.
376fn 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/// A deposit: the boxes it made, and the account's change (which pays the
394/// mixes that follow).
395#[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
403/// Locks one box per register in `owners` (each already re-randomized, so
404/// its `{a, b}` is fresh), paid from `inputs`, with the rest back to
405/// `change_address`. Key-signed, by `signers` keys.
406pub 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/// A mix: the boxes it made (in output order) and the payer's change.
480#[derive(Debug, Clone)]
481pub struct Mix {
482    pub tx: BuiltTransaction,
483    pub fee: u64,
484    pub outputs: Vec<PoolBox>,
485    pub change: Coin,
486    /// Where each input box (in the order given) went among `outputs`.
487    pub moved_to: Vec<usize>,
488}
489
490/// A random scalar in `[1, r)`.
491fn 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
500/// A uniformly random permutation of `0..n`.
501fn permutation(n: usize) -> Vec<usize> {
502    let mut p: Vec<usize> = (0..n).collect();
503    shuffle(&mut p);
504    p
505}
506
507/// `Constr tag fields` as Plutus data.
508fn 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
524/// Mixes `boxes` (two or more, none of them ours to know) into as many fresh
525/// ones, paid by `payer`. Measured against `mix_logic` before it's returned.
526pub 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    // The ledger's order, which the redeemer's proofs follow.
537    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    // ctx = blake2b_256(datums 0..N−1 ‖ values 0..N−1 ‖ mix_box's hash)
558    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    // A mix spends the pool's scripts, so it always puts collateral up; only a
601    // seed, which makes no mixes, ever goes without, and never reaches here.
602    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), &params.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            &params.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    // The finished transaction must pass as it will be sent.
701    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/// A withdraw, ready for giveme.my's collateral signature: the register the
737/// boxes went to and what it holds.
738#[derive(Debug, Clone)]
739pub struct Withdraw {
740    pub tx: BuiltTransaction,
741    pub fee: u64,
742    pub lovelace: u64,
743    pub register: Register,
744}
745
746/// `blake2b_256(serialise_data(outputs) ‖ serialise_data(input refs) ‖
747/// mix_box's hash)`: what an owner's Schnorr proof binds, read from a built
748/// transaction exactly as the ledger hands it to `mix_logic`.
749pub 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
768/// Takes `boxes`, all owned by `owner`, into `destination` (a fresh
769/// re-randomization of the Seedelf register), paying the fee from them, with
770/// giveme.my's collateral. Measured against the scripts before it's returned.
771pub 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                &params.cost_model_v3,
832            )
833        };
834    // Owner { proofs }, one Schnorr proof per box in the ledger's order.
835    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            &params.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    // A placeholder with a real proof's size, for the first build: a proof is
865    // always a compressed point and 32 bytes, so no key is used on one that's
866    // thrown away.
867    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    // Each round's proof is new, and its scalars' sizes move the budget by a
875    // hair, so the transaction declares 1% over what was measured, and is
876    // priced on what it declares (which the ledger charges).
877    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                // Even, so giveme.my's collateral return (5 ₳ − 3/2 × fee) is
902                // whole: an odd fee left it half a lovelace short of what the
903                // ledger asks, which it rounds up (InsufficientCollateral).
904                fee = even(needed);
905            }
906            _ => declared = Some(measured.with_margin(1)),
907        }
908    }
909    bail!("The withdraw's fee did not settle")
910}
911
912/// Mixes one box goes through, fanned out `depth` waves deep, three wide:
913/// every output of a wave is mixed again in the next, so which leaf is ours
914/// stays one of `3^depth` (Lovejoin's `strategy/fanout.ts`).
915pub fn mixes_per_box(depth: u32) -> usize {
916    (3usize.pow(depth) - 1) / 2
917}
918
919/// What the wallet plans on for a 3-box mix before measuring one: preprod's
920/// cost 0.877 ₳, rounded up. One measured 0.82 ₳ against mainnet's scripts.
921/// The wallet pays its mixes itself and never draws on Lovejoin's fee shards,
922/// so the cap Lovejoin puts on a fee a shard pays (1 ₳ on mainnet) doesn't
923/// apply.
924pub const MIX_FEE_ESTIMATE: u64 = 950_000;
925
926/// What a deposit (when the chain has one) and the change the chain leaves
927/// are planned on.
928const DEPOSIT_RESERVE: u64 = 1_500_000;
929
930/// What a box costs at `depth`: the box, and every mix of its fan-out.
931fn per_box(depth: u32, denom: u64) -> u64 {
932    denom + mixes_per_box(depth) as u64 * MIX_FEE_ESTIMATE
933}
934
935/// What mixing one of the wallet's boxes again costs at `depth`: every mix of
936/// its fan-out. It's in the pool already, so there's no box to pay for.
937fn per_box_again(depth: u32) -> u64 {
938    mixes_per_box(depth) as u64 * MIX_FEE_ESTIMATE
939}
940
941/// How many boxes `spare` lovelace pays for, with every mix of a `depth`-deep
942/// fan-out and the deposit: none if it can't pay for one.
943pub fn boxes_affordable(spare: u64, depth: u32, denom: u64) -> usize {
944    (spare.saturating_sub(DEPOSIT_RESERVE) / per_box(depth, denom)) as usize
945}
946
947/// What pays for `boxes` boxes at `depth` ([`boxes_affordable`]'s plan run
948/// backwards): the boxes, every mix, and the deposit and its change. What
949/// the mixes don't use comes back with the change.
950pub fn funding_for(boxes: usize, depth: u32, denom: u64) -> u64 {
951    DEPOSIT_RESERVE + boxes as u64 * per_box(depth, denom)
952}
953
954/// How many of the wallet's boxes `spare` lovelace mixes again at `depth`
955/// ([`again`]): none if it can't pay for one's mixes.
956pub fn again_affordable(spare: u64, depth: u32) -> usize {
957    (spare.saturating_sub(DEPOSIT_RESERVE) / per_box_again(depth)) as usize
958}
959
960/// What pays for mixing `boxes` of the wallet's boxes again at `depth`
961/// ([`again_affordable`]'s plan run backwards): every mix, and the change the
962/// last one leaves, which comes back.
963pub fn again_funding(boxes: usize, depth: u32) -> u64 {
964    DEPOSIT_RESERVE + boxes as u64 * per_box_again(depth)
965}
966
967/// What pays for a chain: the key account's ADA-only `coins`, its
968/// `collateral` (never spent by the chain), and its `address` for change.
969/// The deposit is signed by `deposit_signers` keys (the coins'), each mix by
970/// `mix_signers` (the change's and the collateral's).
971#[derive(Debug, Clone)]
972pub struct Funding {
973    pub coins: Vec<Coin>,
974    /// What every mix puts up. None only for a seed (depth 0), which makes no
975    /// mixes and so spends no script: [`fan_out`] never runs, so never reads it.
976    pub collateral: Option<Coin>,
977    pub address: Address,
978    pub deposit_signers: usize,
979    pub mix_signers: usize,
980}
981
982/// A transaction of a chain, in the order it's sent.
983#[derive(Debug, Clone)]
984pub struct ChainTx {
985    /// `deposit` or `mix`.
986    pub kind: &'static str,
987    pub tx: BuiltTransaction,
988    pub fee: u64,
989}
990
991/// A chain through Lovejoin: its transactions in order, our boxes at the
992/// end of it, and the payer's last change.
993#[derive(Debug, Clone)]
994pub struct Chain {
995    pub txs: Vec<ChainTx>,
996    pub leaves: Vec<PoolBox>,
997    pub change: Coin,
998}
999
1000/// Builds a whole chain before any of it is sent: a deposit of one box per
1001/// register in `owners` from `coins`, then each box fanned out `depth` waves
1002/// deep with fresh boxes drawn at random from `pool` (never one twice). Each
1003/// wave's mixes go in a random order, so the order doesn't point at our
1004/// branch. Every transaction pays from the one before's change; the
1005/// collateral is never spent. Each is measured against the scripts.
1006pub 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    // A mix needs collateral; a seed makes none, so it may go without.
1024    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
1045/// The wallet's own `boxes`, in the pool already, fanned out again `depth`
1046/// waves deep with fresh boxes from `pool`, as [`chain`] fans out a
1047/// deposit's: every output of each wave mixed again in the next, in a random
1048/// order. There's no deposit: the first mix pays from `payer.fee`, and each
1049/// after from the one before's change; the collateral is never spent. For a
1050/// chain cut short, or boxes that haven't moved since.
1051pub 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/// Lovejoin's pool has too few boxes to mix with for a fan-out: `have`
1067/// boxes that aren't ours, where it takes `needed`. Its own type, so a
1068/// caller can tell it from a build that failed (`downcast_ref`).
1069#[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
1087/// The pool's boxes a fan-out of `trees` boxes at `depth` draws from, in a
1088/// random order, `ours` left out: two for every mix, never one twice. Too
1089/// few is a [`PoolShort`].
1090fn fresh_for(trees: usize, depth: u32, pool: &[PoolBox], ours: &[PoolBox]) -> Result<Vec<PoolBox>> {
1091    // Depth 0 is a seed: the deposit alone, with no mixes, so it needs no
1092    // other boxes and works on an empty pool. It hides nothing of its own;
1093    // it puts boxes in for other people to mix with, which is the only way a
1094    // pool can start (the wallet's own boxes never count towards its floor).
1095    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
1115/// Fans each of `start` (all ours) out `depth` waves deep, three wide, with
1116/// boxes from `fresh`, each wave's mixes in a random order so the order
1117/// doesn't point at our branch. The first mix pays from `payer.fee`; each
1118/// after from the one before's change.
1119fn 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    // Every box of every tree in this wave; ours are among them.
1131    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
1168/// Shuffles in place, uniformly (Fisher–Yates). A Seedelf spend's outputs
1169/// go in this order too ([`crate::build::ScriptSpend`]).
1170pub(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}