Skip to main content

midnight_ledger/
construct.rs

1// This file is part of midnight-ledger.
2// Copyright (C) 2025 Midnight Foundation
3// SPDX-License-Identifier: Apache-2.0
4// Licensed under the Apache License, Version 2.0 (the "License");
5// You may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7// http://www.apache.org/licenses/LICENSE-2.0
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14use crate::dust::DustActions;
15use crate::error::{MalformedTransaction, PartitionFailure};
16use crate::structure::{
17    ContractAction, ContractCall, ContractDeploy, Intent, LedgerParameters, MIN_PROOF_SIZE,
18    MaintenanceUpdate, ProofPreimageMarker, ProofPreimageVersioned, SignatureKind, SignaturesValue,
19    SingleUpdate, StandardTransaction, Transaction, UnshieldedOffer, UtxoOutput, UtxoSpend,
20};
21use crate::structure::{
22    EXPECTED_CONTRACT_DEPTH, EXPECTED_OPERATIONS_DEPTH, SegIntent, VERIFIER_KEY_SIZE,
23};
24use crate::structure::{PedersenDowngradeable, ProofKind};
25use base_crypto::cost_model::CostDuration;
26use base_crypto::cost_model::RunningCost;
27use base_crypto::fab::AlignedValue;
28use base_crypto::signatures::Signature;
29use base_crypto::signatures::SigningKey;
30use base_crypto::time::Timestamp;
31use coin_structure::coin::TokenType;
32use coin_structure::contract::ContractAddress;
33use itertools::Itertools;
34use onchain_runtime::context::{Effects, QueryContext, QueryResults};
35use onchain_runtime::error::TranscriptRejected;
36use onchain_runtime::ops::Op;
37use onchain_runtime::result_mode::ResultModeVerify;
38use onchain_runtime::state::{ContractOperation, ContractState, EntryPointBuf};
39use onchain_runtime::transcript::Transcript;
40use rand::{CryptoRng, Rng};
41use serde::{Deserialize, Serialize};
42use serialize::Serializable;
43use std::iter::once;
44use std::ops::Deref;
45use storage::Storable;
46use storage::arena::Sp;
47use storage::db::DB;
48use transient_crypto::commitment::PedersenRandomness;
49use transient_crypto::curve::Fr;
50use transient_crypto::fab::{AlignedValueExt, ValueReprAlignedValue};
51use transient_crypto::hash::transient_commit;
52use transient_crypto::proofs::{KeyLocation, ProofPreimage};
53use transient_crypto::repr::FieldRepr;
54use zswap::{
55    Input as ZswapInput, Offer as ZswapOffer, Output as ZswapOutput, Transient as ZswapTransient,
56};
57
58impl<S: SignatureKind<D>, D: DB> Transaction<S, ProofPreimageMarker, PedersenRandomness, D> {
59    pub fn from_intents(
60        network_id: impl Into<String>,
61        intents: storage::storage::HashMap<
62            u16,
63            Intent<S, ProofPreimageMarker, PedersenRandomness, D>,
64            D,
65        >,
66    ) -> Self {
67        Self::new(network_id, intents, None, storage::storage::HashMap::new())
68    }
69}
70
71impl<S: SignatureKind<D>, D: DB>
72    StandardTransaction<S, ProofPreimageMarker, PedersenRandomness, D>
73{
74    pub fn new(
75        network_id: impl Into<String>,
76        intents: storage::storage::HashMap<
77            u16,
78            Intent<S, ProofPreimageMarker, PedersenRandomness, D>,
79            D,
80        >,
81        guaranteed_coins: Option<ZswapOffer<ProofPreimage, D>>,
82        fallible_coins: storage::storage::HashMap<u16, ZswapOffer<ProofPreimage, D>, D>,
83    ) -> Self {
84        let mut res = StandardTransaction {
85            network_id: network_id.into(),
86            binding_randomness: Default::default(),
87            intents,
88            guaranteed_coins: guaranteed_coins.map(|x| Sp::new(x)),
89            fallible_coins: fallible_coins.into_iter().collect(),
90        };
91        res.recompute_binding_randomness();
92        res
93    }
94
95    pub fn add_calls<P>(
96        &self,
97        rng: &mut (impl Rng + CryptoRng),
98        segment: SegmentSpecifier,
99        calls: &[PrePartitionContractCall<D>],
100        params: &LedgerParameters,
101        ttl: Timestamp,
102        zswap_inputs: &[ZswapInput<ProofPreimage, D>],
103        zswap_outputs: &[ZswapOutput<ProofPreimage, D>],
104        zswap_transients: &[ZswapTransient<ProofPreimage, D>],
105    ) -> Result<Self, PartitionFailure<D>>
106    where
107        P: ContractCallExt<D>,
108    {
109        let pre_transcripts = calls
110            .iter()
111            .map(|call| PreTranscript {
112                comm_comm: Some(communication_commitment(
113                    call.input.clone(),
114                    call.output.clone(),
115                    call.communication_commitment_rand,
116                )),
117                ..call.pre_transcript.clone()
118            })
119            .collect::<Vec<_>>();
120        let partitioned = partition_transcripts(&pre_transcripts, params)?;
121        let segment = match segment {
122            SegmentSpecifier::First => 1,
123            SegmentSpecifier::GuaranteedOnly
124                if partitioned.iter().any(|(_, snd)| snd.is_some()) =>
125            {
126                return Err(PartitionFailure::GuaranteedOnlyUnsatisfied);
127            }
128            SegmentSpecifier::GuaranteedOnly | SegmentSpecifier::Random => rng.r#gen(),
129            SegmentSpecifier::Specific(0) => return Err(PartitionFailure::IllegalSegmentZero),
130            SegmentSpecifier::Specific(seg) => seg,
131        };
132        let prototypes = calls
133            .iter()
134            .zip(partitioned)
135            .map(|(call, (guaranteed, fallible))| ContractCallPrototype {
136                address: call.address,
137                entry_point: call.entry_point.clone(),
138                op: call.op.clone(),
139                guaranteed_public_transcript: guaranteed,
140                fallible_public_transcript: fallible,
141                private_transcript_outputs: call.private_transcript_outputs.clone(),
142                input: call.input.clone(),
143                output: call.output.clone(),
144                communication_commitment_rand: call.communication_commitment_rand,
145                key_location: call.key_location.clone(),
146            })
147            .collect::<Vec<_>>();
148        let zswap_input_is_fallible = zswap_inputs
149            .iter()
150            .map(|inp| {
151                prototypes.iter().any(|pt| {
152                    pt.fallible_public_transcript
153                        .as_ref()
154                        .is_some_and(|pt| pt.effects.claimed_nullifiers.member(&inp.nullifier))
155                })
156            })
157            .collect::<Vec<_>>();
158        let zswap_output_is_fallible = zswap_outputs
159            .iter()
160            .map(|out| {
161                prototypes.iter().any(|pt| {
162                    pt.fallible_public_transcript.as_ref().is_some_and(|pt| {
163                        pt.effects
164                            .claimed_shielded_spends
165                            .union(&pt.effects.claimed_shielded_receives)
166                            .member(&out.coin_com)
167                    })
168                })
169            })
170            .collect::<Vec<_>>();
171        let zswap_transient_is_fallible = zswap_transients
172            .iter()
173            .map(|trans| {
174                prototypes.iter().any(|pt| {
175                    pt.fallible_public_transcript.as_ref().is_some_and(|pt| {
176                        pt.effects
177                            .claimed_shielded_spends
178                            .union(&pt.effects.claimed_shielded_receives)
179                            .member(&trans.coin_com)
180                            || pt.effects.claimed_nullifiers.member(&trans.nullifier)
181                    })
182                })
183            })
184            .collect::<Vec<_>>();
185        let intent_before = self
186            .intents
187            .get(&segment)
188            .map(|i| (*i).clone())
189            .unwrap_or_else(|| Intent::empty(rng, ttl));
190        let intent = prototypes
191            .into_iter()
192            .fold(intent_before, |i, proto| i.add_call::<P>(proto));
193        let mut res = self.set_intent(segment, intent);
194        let guaranteed_coins = ZswapOffer::new(
195            zswap_inputs
196                .iter()
197                .zip(zswap_input_is_fallible.iter())
198                .filter(|(_, f)| !**f)
199                .map(|(i, _)| i.clone())
200                .collect(),
201            zswap_outputs
202                .iter()
203                .zip(zswap_output_is_fallible.iter())
204                .filter(|(_, f)| !**f)
205                .map(|(o, _)| o.clone())
206                .collect(),
207            zswap_transients
208                .iter()
209                .zip(zswap_transient_is_fallible.iter())
210                .filter(|(_, f)| !**f)
211                .map(|(t, _)| t.clone())
212                .collect(),
213        )
214        .map(|o| o.retarget_segment(0));
215        let fallible_coins = ZswapOffer::new(
216            zswap_inputs
217                .iter()
218                .zip(zswap_input_is_fallible.iter())
219                .filter(|(_, f)| **f)
220                .map(|(i, _)| i.clone())
221                .collect(),
222            zswap_outputs
223                .iter()
224                .zip(zswap_output_is_fallible.iter())
225                .filter(|(_, f)| **f)
226                .map(|(o, _)| o.clone())
227                .collect(),
228            zswap_transients
229                .iter()
230                .zip(zswap_transient_is_fallible.iter())
231                .filter(|(_, f)| **f)
232                .map(|(t, _)| t.clone())
233                .collect(),
234        )
235        .map(|o| o.retarget_segment(segment));
236        if let Some(offer) = guaranteed_coins {
237            res.guaranteed_coins = Some(Sp::new(if let Some(old_offer) = &res.guaranteed_coins {
238                old_offer.merge(&offer).map_err(PartitionFailure::Merge)?
239            } else {
240                offer
241            }));
242        }
243        if let Some(offer) = fallible_coins {
244            let new_offer = if let Some(old_offer) = res.fallible_coins.get(&segment) {
245                old_offer.merge(&offer).map_err(PartitionFailure::Merge)?
246            } else {
247                offer
248            };
249            res.fallible_coins = res.fallible_coins.insert(segment, new_offer);
250        }
251        res.recompute_binding_randomness();
252        Ok(res)
253    }
254
255    pub fn set_intent(
256        &self,
257        segment: u16,
258        intent: Intent<S, ProofPreimageMarker, PedersenRandomness, D>,
259    ) -> Self {
260        let mut res = self.clone();
261        res.intents = self.intents.insert(segment, intent);
262        res.recompute_binding_randomness();
263        res
264    }
265
266    pub fn recompute_binding_randomness(&mut self) {
267        self.binding_randomness = self
268            .guaranteed_coins
269            .as_ref()
270            .map(|o| o.binding_randomness())
271            .unwrap_or_else(|| PedersenRandomness::from(0))
272            + self
273                .fallible_coins
274                .values()
275                .fold(PedersenRandomness::from(0), |acc, o| {
276                    acc + o.binding_randomness()
277                })
278            + self
279                .intents
280                .values()
281                .map(|i| i.binding_randomness())
282                .fold(0.into(), |a, b| a + b);
283    }
284}
285
286impl<D: DB> ContractDeploy<D> {
287    pub fn new<R: Rng + CryptoRng + ?Sized>(rng: &mut R, initial_state: ContractState<D>) -> Self {
288        ContractDeploy {
289            initial_state,
290            nonce: rng.r#gen(),
291        }
292    }
293}
294
295impl<D: DB> MaintenanceUpdate<D> {
296    pub fn new(address: ContractAddress, updates: Vec<SingleUpdate>, counter: u32) -> Self {
297        MaintenanceUpdate {
298            address,
299            updates: updates.into(),
300            counter,
301            signatures: vec![].into(),
302        }
303    }
304
305    pub fn add_signature(mut self, idx: u32, signature: Signature) -> Self {
306        self.signatures = self
307            .signatures
308            .push(SignaturesValue(idx, signature))
309            .iter_deref()
310            .cloned()
311            .sorted()
312            .collect();
313        self
314    }
315}
316
317impl<S: SignatureKind<D>, D: DB> Intent<S, ProofPreimageMarker, PedersenRandomness, D> {
318    pub fn empty<R: Rng + CryptoRng + ?Sized>(
319        rng: &mut R,
320        ttl: Timestamp,
321    ) -> Intent<S, ProofPreimageMarker, PedersenRandomness, D> {
322        Intent::new(rng, None, None, vec![], vec![], vec![], None, ttl)
323    }
324
325    #[allow(clippy::too_many_arguments)]
326    pub fn new<R: Rng + CryptoRng + ?Sized>(
327        rng: &mut R,
328        guaranteed_unshielded_offer: Option<UnshieldedOffer<S, D>>,
329        fallible_unshielded_offer: Option<UnshieldedOffer<S, D>>,
330        calls: Vec<ContractCallPrototype<D>>,
331        updates: Vec<MaintenanceUpdate<D>>,
332        deploys: Vec<ContractDeploy<D>>,
333        dust_actions: Option<DustActions<S, ProofPreimageMarker, D>>,
334        ttl: Timestamp,
335    ) -> Intent<S, ProofPreimageMarker, PedersenRandomness, D> {
336        let intent = Intent {
337            guaranteed_unshielded_offer: guaranteed_unshielded_offer.map(|x| Sp::new(x)),
338            fallible_unshielded_offer: fallible_unshielded_offer.map(|x| Sp::new(x)),
339            actions: vec![].into(),
340            dust_actions: dust_actions.map(Sp::new),
341            ttl,
342            binding_commitment: rng.r#gen(),
343        };
344
345        let intent = calls
346            .into_iter()
347            .fold(intent, |acc, x| acc.add_call::<ProofPreimage>(x));
348
349        let intent = updates
350            .into_iter()
351            .fold(intent, |acc, x| acc.add_maintenance_update(x));
352
353        deploys.into_iter().fold(intent, |acc, x| acc.add_deploy(x))
354    }
355}
356
357impl<
358    S: SignatureKind<D>,
359    P: ProofKind<D>,
360    B: Storable<D> + PedersenDowngradeable<D> + Serializable,
361    D: DB,
362> Intent<S, P, B, D>
363{
364    #[allow(clippy::result_large_err)]
365    pub fn sign(
366        mut self,
367        rng: &mut (impl Rng + CryptoRng),
368        segment_id: u16,
369        guaranteed_signing_keys: &[SigningKey],
370        fallible_signing_keys: &[SigningKey],
371        dust_registration_signing_keys: &[SigningKey],
372    ) -> Result<Self, MalformedTransaction<D>>
373    where
374        UnshieldedOffer<S, D>: Clone,
375        P: ProofKind<D>,
376    {
377        let data = self
378            .erase_proofs()
379            .erase_signatures()
380            .data_to_sign(segment_id);
381
382        let mut sign_unshielded_offers =
383            |unshielded_offer: &mut Option<Sp<UnshieldedOffer<S, D>, D>>,
384             signing_keys: &[SigningKey]|
385             -> Result<(), MalformedTransaction<D>> {
386                if let Some(offer) = unshielded_offer {
387                    let signatures: Vec<<S as SignatureKind<D>>::Signature<SegIntent<D>>> = offer
388                        .inputs
389                        .iter()
390                        .zip(signing_keys)
391                        .map(|(spend, sk)| {
392                            if spend.owner != sk.verifying_key() {
393                                return Err(MalformedTransaction::<D>::IntentSignatureKeyMismatch);
394                            }
395                            Ok(S::sign(sk, rng, &data))
396                        })
397                        .collect::<Result<Vec<_>, _>>()?;
398
399                    let mut new = (*offer).deref().clone();
400                    new.add_signatures(signatures);
401                    *unshielded_offer = Some(Sp::new(new.clone()));
402                }
403                Ok(())
404            };
405
406        sign_unshielded_offers(
407            &mut self.guaranteed_unshielded_offer,
408            guaranteed_signing_keys,
409        )?;
410        sign_unshielded_offers(&mut self.fallible_unshielded_offer, fallible_signing_keys)?;
411
412        if let Some(da) = self.dust_actions {
413            let registrations = da
414                .registrations
415                .iter()
416                .zip(dust_registration_signing_keys.iter())
417                .map(|(reg, sk)| {
418                    let mut reg = (*reg).clone();
419                    if reg.night_key != sk.verifying_key() {
420                        return Err(MalformedTransaction::<D>::IntentSignatureKeyMismatch);
421                    }
422                    reg.signature = Some(Sp::new(S::sign(sk, rng, &data)));
423                    Ok(reg)
424                })
425                .collect::<Result<_, _>>()?;
426            self.dust_actions = Some(Sp::new(DustActions {
427                registrations,
428                spends: da.spends.clone(),
429                ctime: da.ctime,
430            }));
431        }
432
433        Ok(self)
434    }
435}
436
437impl<S: SignatureKind<D>, D: DB> Transaction<S, ProofPreimageMarker, PedersenRandomness, D> {
438    pub fn new(
439        network_id: impl Into<String>,
440        intents: storage::storage::HashMap<
441            u16,
442            Intent<S, ProofPreimageMarker, PedersenRandomness, D>,
443            D,
444        >,
445        guaranteed_coins: Option<ZswapOffer<ProofPreimage, D>>,
446        fallible_coins: storage::storage::HashMap<u16, ZswapOffer<ProofPreimage, D>, D>,
447    ) -> Self {
448        let mut stx = StandardTransaction {
449            network_id: network_id.into(),
450            intents,
451            guaranteed_coins: guaranteed_coins.map(|x| Sp::new(x.retarget_segment(0))),
452            fallible_coins: fallible_coins
453                .into_iter()
454                .map(|(seg, offer)| (seg, offer.retarget_segment(seg)))
455                .collect(),
456            binding_randomness: Default::default(),
457        };
458        stx.recompute_binding_randomness();
459        Transaction::Standard(stx)
460    }
461}
462
463#[derive_where::derive_where(Clone, Debug)]
464pub struct PrePartitionContractCall<D: DB> {
465    pub address: ContractAddress,
466    pub entry_point: EntryPointBuf,
467    pub op: ContractOperation,
468    pub pre_transcript: PreTranscript<D>,
469    pub private_transcript_outputs: Vec<AlignedValue>,
470    pub input: AlignedValue,
471    pub output: AlignedValue,
472    pub communication_commitment_rand: Fr,
473    pub key_location: KeyLocation,
474}
475
476#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
477#[serde(tag = "tag", content = "value", rename_all = "camelCase")]
478pub enum SegmentSpecifier {
479    First,
480    GuaranteedOnly,
481    Random,
482    Specific(u16),
483}
484
485#[derive(Clone, Debug)]
486pub struct ContractCallPrototype<D: DB> {
487    pub address: ContractAddress,
488    pub entry_point: EntryPointBuf,
489    pub op: ContractOperation,
490    pub guaranteed_public_transcript: Option<Transcript<D>>,
491    pub fallible_public_transcript: Option<Transcript<D>>,
492    pub private_transcript_outputs: Vec<AlignedValue>,
493    pub input: AlignedValue,
494    pub output: AlignedValue,
495    pub communication_commitment_rand: Fr,
496    pub key_location: KeyLocation,
497}
498
499pub trait ContractCallExt<D: DB> {
500    type AssociatedProof;
501    fn construct_proof(
502        call: &ContractCallPrototype<D>,
503        communication_commitment: Fr,
504    ) -> ProofPreimageVersioned;
505}
506
507impl<D: DB> ContractCallExt<D> for ProofPreimage {
508    type AssociatedProof = ProofPreimage;
509    fn construct_proof(
510        call: &ContractCallPrototype<D>,
511        communication_commitment: Fr,
512    ) -> ProofPreimageVersioned {
513        let inputs = ValueReprAlignedValue(call.input.clone()).field_vec();
514        let mut private_transcript = Vec::with_capacity(
515            call.private_transcript_outputs
516                .iter()
517                .map(|o| o.value_only_field_size())
518                .sum(),
519        );
520        for o in call.private_transcript_outputs.iter() {
521            o.value_only_field_repr(&mut private_transcript);
522        }
523        let public_transcript_iter = call
524            .guaranteed_public_transcript
525            .iter()
526            .flat_map(|t| t.program.iter_deref())
527            .chain(
528                call.fallible_public_transcript
529                    .iter()
530                    .flat_map(|t| t.program.iter_deref()),
531            );
532
533        let public_transcript_ops: Vec<_> = public_transcript_iter.collect();
534
535        let mut public_transcript_inputs =
536            Vec::with_capacity(public_transcript_ops.iter().map(|op| op.field_size()).sum());
537
538        for op in &public_transcript_ops {
539            op.field_repr(&mut public_transcript_inputs);
540        }
541
542        let mut public_transcript_outputs = Vec::new();
543        for op in &public_transcript_ops {
544            if let Op::Popeq { result, .. } = op {
545                result.value_only_field_repr(&mut public_transcript_outputs);
546            }
547        }
548
549        // This gets populated correctly during proving, ensuring it correctly uses the updated
550        // transcripts and costs.
551        let binding_input = 0u8.into();
552
553        let proof = ProofPreimage {
554            inputs,
555            private_transcript,
556            public_transcript_inputs,
557            public_transcript_outputs,
558            binding_input,
559            communications_commitment: Some((
560                communication_commitment,
561                call.communication_commitment_rand,
562            )),
563            key_location: call.key_location.clone(),
564        };
565
566        ProofPreimageVersioned::V2(std::sync::Arc::new(proof))
567    }
568}
569
570impl<D: DB> ContractCall<ProofPreimageMarker, D> {
571    pub fn new(
572        address: ContractAddress,
573        entry_point: EntryPointBuf,
574        guaranteed_transcript: Option<Transcript<D>>,
575        fallible_transcript: Option<Transcript<D>>,
576        communication_commitment: Fr,
577        proof: ProofPreimageVersioned,
578    ) -> Self {
579        ContractCall {
580            address,
581            entry_point,
582            guaranteed_transcript: guaranteed_transcript.map(Sp::new),
583            fallible_transcript: fallible_transcript.map(Sp::new),
584            communication_commitment,
585            proof,
586        }
587    }
588}
589
590impl<S: SignatureKind<D>, D: DB> Intent<S, ProofPreimageMarker, PedersenRandomness, D> {
591    pub fn add_call<P>(&self, call: ContractCallPrototype<D>) -> Self
592    where
593        P: ContractCallExt<D>,
594    {
595        let mut io_repr = Vec::with_capacity(
596            call.input.value_only_field_size() + call.output.value_only_field_size(),
597        );
598        call.input.value_only_field_repr(&mut io_repr);
599        call.output.value_only_field_repr(&mut io_repr);
600        let communication_commitment =
601            transient_commit(&io_repr, call.communication_commitment_rand);
602
603        let proof = P::construct_proof(&call, communication_commitment);
604        let call = ContractCall {
605            address: call.address,
606            entry_point: call.entry_point,
607            guaranteed_transcript: call.guaranteed_public_transcript.map(|x| Sp::new(x)),
608            fallible_transcript: call.fallible_public_transcript.map(|x| Sp::new(x)),
609            communication_commitment,
610            proof,
611        };
612        // We add the call:
613        // - Directly before the first call *claimed* by it
614        // - At the end otherwise.
615        let mut actions = Vec::new();
616        let mut already_inserted = false;
617        fn references<D: DB>(
618            caller: &ContractCall<ProofPreimageMarker, D>,
619            callee: &ContractAction<ProofPreimageMarker, D>,
620        ) -> bool {
621            match callee {
622                ContractAction::Call(call) => caller
623                    .guaranteed_transcript
624                    .iter()
625                    .chain(caller.fallible_transcript.iter())
626                    .flat_map(|t| {
627                        t.effects
628                            .claimed_contract_calls
629                            .iter()
630                            .map(|x| (*x).deref().into_inner())
631                    })
632                    .any(|(_seq, addr, ep_hash, cc)| {
633                        addr == call.address
634                            && cc == call.communication_commitment
635                            && ep_hash == call.entry_point.ep_hash()
636                    }),
637                _ => false,
638            }
639        }
640        for c in self.actions.iter_deref().cloned() {
641            if !already_inserted && references(&call, &c) {
642                actions.push(call.clone().into());
643                already_inserted = true;
644            }
645            actions.push(c);
646        }
647        if !already_inserted {
648            actions.push(call.into());
649        }
650        Intent {
651            guaranteed_unshielded_offer: self.guaranteed_unshielded_offer.clone(),
652            fallible_unshielded_offer: self.fallible_unshielded_offer.clone(),
653            actions: actions.into(),
654            dust_actions: self.dust_actions.clone(),
655            ttl: self.ttl,
656            binding_commitment: self.binding_commitment,
657        }
658    }
659
660    pub fn add_deploy(&self, deploy: ContractDeploy<D>) -> Self {
661        Intent {
662            guaranteed_unshielded_offer: self.guaranteed_unshielded_offer.clone(),
663            fallible_unshielded_offer: self.fallible_unshielded_offer.clone(),
664            actions: self
665                .actions
666                .iter_deref()
667                .cloned()
668                .chain(once(deploy.into()))
669                .collect(),
670            dust_actions: self.dust_actions.clone(),
671            ttl: self.ttl,
672            binding_commitment: self.binding_commitment,
673        }
674    }
675
676    pub fn add_maintenance_update(&self, upd: MaintenanceUpdate<D>) -> Self {
677        Intent {
678            guaranteed_unshielded_offer: self.guaranteed_unshielded_offer.clone(),
679            fallible_unshielded_offer: self.fallible_unshielded_offer.clone(),
680            actions: self
681                .actions
682                .iter_deref()
683                .cloned()
684                .chain(once(upd.into()))
685                .collect(),
686            dust_actions: self.dust_actions.clone(),
687            ttl: self.ttl,
688            binding_commitment: self.binding_commitment,
689        }
690    }
691
692    pub fn binding_randomness(&self) -> PedersenRandomness {
693        self.binding_commitment
694    }
695}
696
697#[derive(Debug)]
698pub struct PreTranscript<D: DB> {
699    pub context: QueryContext<D>,
700    pub program: Vec<Op<ResultModeVerify, D>>,
701    pub comm_comm: Option<Fr>,
702}
703
704impl<D: DB> Clone for PreTranscript<D> {
705    fn clone(&self) -> Self {
706        PreTranscript {
707            context: self.context.clone(),
708            program: self.program.clone(),
709            comm_comm: self.comm_comm,
710        }
711    }
712}
713
714impl<D: DB> PreTranscript<D> {
715    fn no_checkpoints(&self) -> usize {
716        self.program
717            .iter()
718            .filter(|op| matches!(op, Op::Ckpt))
719            .count()
720    }
721
722    // 0-indexed!
723    fn run_to_ckpt_no(
724        &self,
725        mut n: usize,
726        params: &LedgerParameters,
727    ) -> Result<QueryResults<ResultModeVerify, D>, TranscriptRejected<D>> {
728        n += 1;
729        let prog = self
730            .program
731            .iter()
732            .cloned()
733            .take_while(|op| {
734                if matches!(op, Op::Ckpt) {
735                    n -= 1;
736                    n != 0
737                } else {
738                    true
739                }
740            })
741            .collect::<Vec<_>>();
742        self.context
743            .query(&prog, None, &params.cost_model.runtime_cost_model)
744    }
745
746    fn guaranteed_budget(&self, params: &LedgerParameters) -> CostDuration {
747        let est_size = MIN_PROOF_SIZE
748            + Serializable::serialized_size(&(
749                ContractAddress::default(),
750                Effects::<D>::default(),
751                &self.program.iter().collect::<Vec<_>>(),
752                &self.comm_comm,
753            ));
754        params.limits.time_to_dismiss_per_byte * est_size as u64
755    }
756
757    // 1-indexed!
758    #[allow(clippy::type_complexity)]
759    fn split_at(
760        &self,
761        mut n: usize,
762        params: &LedgerParameters,
763    ) -> Result<(Option<Transcript<D>>, Option<Transcript<D>>), TranscriptRejected<D>> {
764        let mut prog_guaranteed = Vec::new();
765        let mut prog_fallible = Vec::new();
766        for op in self.program.iter() {
767            if n > 0 && matches!(op, Op::Ckpt) {
768                n -= 1;
769            }
770            if n == 0 {
771                prog_fallible.push(op.clone());
772            } else {
773                prog_guaranteed.push(op.clone());
774            }
775        }
776        let guaranteed_res = self.context.query(
777            &prog_guaranteed,
778            None,
779            &params.cost_model.runtime_cost_model,
780        )?;
781        let mut continuation_context = guaranteed_res.context.clone();
782        continuation_context.effects = Effects::default();
783        let fallible_res = continuation_context.query(
784            &prog_fallible,
785            None,
786            &params.cost_model.runtime_cost_model,
787        )?;
788        let mk_transcript = |prog: Vec<Op<ResultModeVerify, D>>,
789                             res: QueryResults<ResultModeVerify, D>| {
790            if prog.is_empty() {
791                None
792            } else {
793                Some(Transcript {
794                    gas: res.gas_heuristic(params, false, 0),
795                    effects: res.context.effects.clone(),
796                    program: prog.into(),
797                    version: Some(Sp::new(Transcript::<D>::VERSION)),
798                })
799            }
800        };
801        Ok((
802            mk_transcript(prog_guaranteed, guaranteed_res),
803            mk_transcript(prog_fallible, fallible_res),
804        ))
805    }
806}
807
808trait QueryResultsExt {
809    fn gas_heuristic(
810        &self,
811        params: &LedgerParameters,
812        include_external: bool,
813        public_input_count: usize,
814    ) -> RunningCost;
815}
816
817fn test_tx_from_unshielded_offer<D: DB>(
818    offer: UnshieldedOffer<Signature, D>,
819) -> Transaction<Signature, ProofPreimageMarker, PedersenRandomness, D> {
820    use rand::{SeedableRng, rngs::StdRng};
821    let intent = Intent::new(
822        &mut StdRng::seed_from_u64(0x42),
823        Some(offer),
824        None,
825        Vec::new(),
826        Vec::new(),
827        Vec::new(),
828        None,
829        Timestamp::from_secs(0),
830    );
831    Transaction::from_intents("local-test", [(1, intent)].into_iter().collect())
832}
833impl<D: DB> QueryResultsExt for QueryResults<ResultModeVerify, D> {
834    fn gas_heuristic(
835        &self,
836        params: &LedgerParameters,
837        include_external: bool,
838        public_input_count: usize,
839    ) -> RunningCost {
840        let transcript_gas_cost_with_overhead = self.gas_cost * 1.2;
841        if !include_external {
842            return transcript_gas_cost_with_overhead;
843        }
844        let expected_unshielded_inputs_overhead = self
845            .context
846            .effects
847            .unshielded_inputs
848            .keys()
849            .map(|tt| {
850                let TokenType::Unshielded(tt) = tt else {
851                    unreachable!()
852                };
853                let input = UtxoSpend {
854                    intent_hash: Default::default(),
855                    output_no: 0,
856                    owner: Default::default(),
857                    type_: tt,
858                    value: 1,
859                };
860                let output = UtxoOutput {
861                    owner: Default::default(),
862                    type_: tt,
863                    value: 1,
864                };
865                let offer = UnshieldedOffer::<_, D> {
866                    inputs: vec![input].into(),
867                    outputs: vec![output].into(),
868                    signatures: Default::default(),
869                };
870                let tx = test_tx_from_unshielded_offer(offer);
871                tx.cost(params, false)
872                    .map(RunningCost::from)
873                    .unwrap_or(RunningCost::ZERO)
874            })
875            .sum();
876        let expected_unshielded_outputs_overhead = self
877            .context
878            .effects
879            .unshielded_outputs
880            .keys()
881            .map(|tt| {
882                let TokenType::Unshielded(tt) = tt else {
883                    unreachable!()
884                };
885                let output = UtxoOutput {
886                    owner: Default::default(),
887                    type_: tt,
888                    value: 1,
889                };
890                let offer = UnshieldedOffer::<_, D> {
891                    inputs: Default::default(),
892                    outputs: vec![output].into(),
893                    signatures: Default::default(),
894                };
895                let tx = test_tx_from_unshielded_offer(offer);
896                tx.cost(params, false)
897                    .map(RunningCost::from)
898                    .unwrap_or(RunningCost::ZERO)
899            })
900            .sum();
901        let proof_verify = params.cost_model.proof_verify(public_input_count)
902            + params.cost_model.cell_read(VERIFIER_KEY_SIZE as u64)
903            + params.cost_model.map_index(EXPECTED_CONTRACT_DEPTH)
904            + params.cost_model.map_index(EXPECTED_OPERATIONS_DEPTH);
905        transcript_gas_cost_with_overhead
906            + expected_unshielded_inputs_overhead
907            + expected_unshielded_outputs_overhead
908            + proof_verify
909    }
910}
911
912pub fn communication_commitment(input: AlignedValue, output: AlignedValue, rand: Fr) -> Fr {
913    transient_commit(&AlignedValue::concat([&input, &output]), rand)
914}
915
916pub type TranscriptPair<D> = (Option<Transcript<D>>, Option<Transcript<D>>);
917
918pub fn partition_transcripts<D: DB>(
919    calls: &[PreTranscript<D>],
920    params: &LedgerParameters,
921) -> Result<Vec<TranscriptPair<D>>, PartitionFailure<D>> {
922    let n = calls.len();
923    // Step 1: Generate a call graph between `calls`. Assert that this is a forest (no cycles,
924    //      no multiple parents).
925
926    // Gather full runs to observe all calls
927    let no_ckpts = calls
928        .iter()
929        .map(PreTranscript::no_checkpoints)
930        .collect::<Vec<_>>();
931    let full_runs = calls
932        .iter()
933        .zip(no_ckpts.iter())
934        .map(|(pt, n)| pt.run_to_ckpt_no(*n, params))
935        .collect::<Result<Vec<_>, _>>()?;
936    // Graph is a Vec of Vecs, where call_graph[i] = [a, b, c] means that calls[i] calls calls[a],
937    // calls[b] and calls[c]
938    let call_graph = full_runs
939        .iter()
940        .map(|qr| {
941            let claimed_commitments = qr
942                .context
943                .effects
944                .claimed_contract_calls
945                .iter()
946                .map(|sp| (*sp).deref().into_inner().3)
947                .collect::<Vec<_>>();
948            (0..n)
949                .filter(|i| {
950                    calls[*i]
951                        .comm_comm
952                        .map(|comm| claimed_commitments.contains(&comm))
953                        .unwrap_or(false)
954                })
955                .collect::<Vec<_>>()
956        })
957        .collect::<Vec<_>>();
958    // Identify cycles by starting at each node in turn, and asserting that the set of reachable
959    // nodes does not contain itself
960    //
961    // Note that this also identifies some non-cycles, such as A -> B, A -> C, B -> D, B -> D,
962    // but this is fine as these are also not allowed.
963    for i in 0..n {
964        let mut visited = vec![i];
965        let mut reachable = call_graph[i].clone();
966        while let Some(next) = reachable.pop() {
967            if visited.contains(&next) {
968                return Err(PartitionFailure::NonForest);
969            }
970            visited.push(next);
971            reachable.extend(&call_graph[next]);
972        }
973    }
974
975    // Step 2: Identify root nodes in the DAG.
976    // Identify multiple parents at the same time. For each node, count how many other nodes
977    // reference it. If that's 0, it's a root node. If it's >1, we don't have a forest.
978    let n_callers = (0..n)
979        .map(|i| (0..n).filter(|j| call_graph[*j].contains(&i)).count())
980        .collect::<Vec<_>>();
981    if n_callers.iter().any(|n| *n > 1) {
982        return Err(PartitionFailure::NonForest);
983    }
984    let root_nodes = n_callers
985        .iter()
986        .enumerate()
987        .filter(|(_, n)| **n == 0)
988        .map(|(i, _)| i)
989        .collect::<Vec<_>>();
990
991    // Step 3: For each root node, compute the guaranteed section budget of its closure.
992    let closures = root_nodes
993        .iter()
994        .map(|r| {
995            let mut visited = vec![];
996            let mut frontier = vec![*r];
997            while let Some(item) = frontier.pop() {
998                visited.push(item);
999                frontier.extend(&call_graph[item]);
1000            }
1001            visited
1002        })
1003        .collect::<Vec<_>>();
1004    let mut closure_budgets = closures
1005        .iter()
1006        .map(|closure| {
1007            closure
1008                .iter()
1009                .map(|i| calls[*i].guaranteed_budget(params))
1010                .sum::<CostDuration>()
1011        })
1012        .collect::<Vec<_>>();
1013    // Allow creep-up to the min time to dismiss, up to 70% usage
1014    let spare_min_time_to_dismiss =
1015        params.limits.min_time_to_dismiss * 0.7f64 - closure_budgets.iter().copied().sum();
1016    for budget in closure_budgets.iter_mut() {
1017        *budget += spare_min_time_to_dismiss * (1.0 / closures.len() as f64);
1018    }
1019
1020    // Step 4: Partition the root nodes:
1021    //      4a: Split root nodes on `ckpt`s.
1022    //      4b. Run up to `ckpt` and determine calls that are included. Run those entirely.
1023    //      4c. Determine the latest `ckpt` for which this fits into the previously computed
1024    //          budget.
1025
1026    // preliminary_results is a vec that, for each root, contains a pair consisting of:
1027    // the number of checkpoints to include in the guaranteed section (0 indicating entirely in the
1028    // fallible section), and a vec of the indices of the callees that are included in the
1029    // guaranteed section (a subset of the corresponding entry in call_graph).
1030    let preliminary_results = root_nodes
1031        .iter()
1032        .enumerate()
1033        .map(|(root_n, &root)| {
1034            // Run through the number of sections to put into the guaranteed transcript.
1035            // Start with the number of checkpoints + 1 (we have one more section than checkpoints),
1036            // end at 1. If none pass, 0 make it into the guaranteed section.
1037            for n in (1..no_ckpts[root] + 2).rev() {
1038                let partial_res = calls[root].run_to_ckpt_no(n - 1, params)?;
1039                let claimed = partial_res
1040                    .context
1041                    .effects
1042                    .claimed_contract_calls
1043                    .iter()
1044                    .map(|sp| (*sp).deref().into_inner().3)
1045                    .collect::<Vec<_>>();
1046                let claimed_idx = calls
1047                    .iter()
1048                    .enumerate()
1049                    .filter(|(_, pt)| {
1050                        pt.comm_comm
1051                            .map(|cc| claimed.contains(&cc))
1052                            .unwrap_or(false)
1053                    })
1054                    .map(|(i, _)| i)
1055                    .collect::<Vec<_>>();
1056                let public_input_count = calls[root].program.field_size() + 2;
1057                let mut required_budget = partial_res
1058                    .gas_heuristic(params, true, public_input_count)
1059                    .max_time();
1060                let mut frontier = claimed_idx.clone();
1061                while let Some(next) = frontier.pop() {
1062                    required_budget += full_runs[next]
1063                        .gas_heuristic(params, true, public_input_count)
1064                        .max_time();
1065                    frontier.extend(&call_graph[next]);
1066                }
1067                if required_budget <= closure_budgets[root_n] {
1068                    return Ok((n, claimed_idx));
1069                }
1070            }
1071            Ok::<_, PartitionFailure<D>>((0, vec![]))
1072        })
1073        .collect::<Result<Vec<_>, _>>()?;
1074    let mut sections_in_guaranteed = vec![0; n];
1075    for (root_n, &root) in root_nodes.iter().enumerate() {
1076        sections_in_guaranteed[root] = preliminary_results[root_n].0;
1077        let mut frontier = preliminary_results[root_n].1.clone();
1078        while let Some(next) = frontier.pop() {
1079            sections_in_guaranteed[next] = no_ckpts[next] + 1;
1080            frontier.extend(&call_graph[next]);
1081        }
1082    }
1083
1084    Ok(sections_in_guaranteed
1085        .into_iter()
1086        .enumerate()
1087        .map(|(i, sections)| calls[i].split_at(sections, params))
1088        .collect::<Result<Vec<_>, _>>()?)
1089}