Skip to main content

snarkvm_synthesizer_process/trace/translation/
assignment.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkVM library.
3
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
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use circuit::{Aleo, Field as CircuitField, traits::ToFields};
17
18use super::*;
19
20/// Computes the ID of a console record (dynamic or external) given its field representation.
21pub fn compute_console_dynamic_or_external_record_id<N: Network>(
22    function_id: Field<N>,
23    record_fields: Vec<Field<N>>,
24    tvk: Field<N>,
25    index: U16<N>,
26) -> Result<Field<N>> {
27    let mut preimage = Vec::new();
28    preimage.push(function_id);
29    preimage.extend(record_fields);
30    preimage.push(tvk);
31    preimage.push(index.to_field()?);
32
33    N::hash_psd8(&preimage)
34}
35
36/// Computes the ID of a circuit record (dynamic or external) given its field representation.
37fn compute_circuit_dynamic_or_external_record_id<A: Aleo>(
38    function_id: CircuitField<A>,
39    record_fields: Vec<CircuitField<A>>,
40    tvk: CircuitField<A>,
41    index: CircuitField<A>,
42) -> CircuitField<A> {
43    let mut preimage = Vec::new();
44    preimage.push(function_id);
45    preimage.extend(record_fields);
46    preimage.push(tvk);
47    preimage.push(index);
48
49    A::hash_psd8(&preimage)
50}
51
52/// Data collected during execution and used to prove record translation in dynamic calls.
53#[derive(Clone, Debug)]
54pub struct TranslationAssignment<N: Network> {
55    /// The static record (whether external or not).
56    pub(crate) record_static: Record<N, Plaintext<N>>,
57    /// The dynamic record.
58    pub(crate) record_dynamic: DynamicRecord<N>,
59    /// The ID of the program where the static record is defined (whether external or not).
60    pub(crate) program_id: ProgramID<N>,
61    /// The function ID of the callee in the dynamic call.
62    pub(crate) function_id: Field<N>,
63    /// The name of the static record.
64    pub(crate) record_name: Identifier<N>,
65    /// True if the translation is for an input to `call.dynamic` (the callee consumes the static record;
66    /// its serial number is used as the input ID). False if the translation is for an output of
67    /// `call.dynamic` (the callee produces the static record; its commitment is used as the output ID).
68    pub(crate) is_to_static: bool,
69    /// Whether the value type corresponding to the static record is `Record` or `ExternalRecord`.
70    pub(crate) is_external_record: bool,
71    /// The view key of the transition containing the dynamic call.
72    pub(crate) tvk: Field<N>,
73    /// The record view key of the static record. Irrelevant if `is_external_record` is true.
74    pub(crate) record_view_key: Option<Field<N>>,
75    /// The additional point used to produce the serial number.
76    /// Irrelevant if `is_to_static` is false or `is_external_record` is true.
77    pub(crate) gamma: Option<Group<N>>,
78    /// Index of the input operand or output destination that contains the (dynamic and static) record.
79    /// Note: The first three call.dynamic operands are reserved for call-related data,
80    /// however this operand index still starts at 0 and is the same for caller and callee.
81    pub(crate) record_register_index: u16,
82    /// The ID of the dynamic record.
83    pub(crate) id_dynamic: Field<N>,
84    /// The ID of the static record:
85    /// - If the static record is external, this is its `InputID` = `OutputID`.
86    /// - If the static record is not external, this is:
87    ///   - Its `InputID`, i.e. its serial number, if the record is an input.
88    ///   - Its `OutputID`, i.e. its commitment, if the record is an output.
89    pub(crate) id_static: Field<N>,
90}
91
92impl<N: Network> TranslationAssignment<N> {
93    /// Initializes a new translation assignment.
94    #[allow(clippy::too_many_arguments)]
95    pub fn new(
96        record_static: Record<N, Plaintext<N>>,
97        record_dynamic: DynamicRecord<N>,
98        program_id: ProgramID<N>,
99        function_id: Field<N>,
100        record_name: Identifier<N>,
101        is_to_static: bool,
102        is_external_record: bool,
103        tvk: Field<N>,
104        record_view_key: Option<Field<N>>,
105        gamma: Option<Group<N>>,
106        record_register_index: u16,
107        id_dynamic: Field<N>,
108        id_static: Field<N>,
109    ) -> Self {
110        Self {
111            record_static,
112            record_dynamic,
113            program_id,
114            function_id,
115            record_name,
116            is_to_static,
117            is_external_record,
118            tvk,
119            record_view_key,
120            gamma,
121            record_register_index,
122            id_dynamic,
123            id_static,
124        }
125    }
126
127    // Internal auxiliary function which actually constructs the translation
128    // circuit in `A`. The publicly exposed function `to_circuit_assignment`
129    // ejects the resulting `Assignment` from the R1CS, but having direct access
130    // to `A` while the constraint system is still loaded facilitates testing.
131    pub(crate) fn to_circuit_assignment_internal<A: Aleo<Network = N>>(&self, translation_index: u16) -> Result<()> {
132        // Ensure the circuit environment is clean.
133        ensure!(
134            A::count() == (0, 1, 0, 0, (0, 0, 0)),
135            "Circuit environment is not clean: expected (0, 1, 0, 0, (0, 0, 0)), got {:?}",
136            A::count()
137        );
138        A::reset();
139
140        // ******** Constants
141
142        // Inject the program ID as `Mode::Constant`.
143        let circuit_program_id = circuit::ProgramID::<A>::constant(self.program_id);
144
145        // Inject the record name as `Mode::Constant`.
146        let circuit_record_name = circuit::Identifier::<A>::constant(self.record_name);
147
148        // ******** Public inputs
149
150        // Inject the translation-direction flag as `Mode::Public`.
151        let circuit_is_to_static = circuit::Boolean::<A>::new(circuit::Mode::Public, self.is_to_static);
152
153        // Inject the external-record flag as `Mode::Public`.
154        let circuit_is_external_record = circuit::Boolean::<A>::new(circuit::Mode::Public, self.is_external_record);
155
156        // Inject the calling function id as `Mode::Public`.
157        let circuit_function_id = circuit::Field::<A>::new(circuit::Mode::Public, self.function_id);
158
159        // Inject the translation index as `Mode::Public`.
160        // Note that although the index is not explicitly used in the circuit, the prover and verifier must use the same value for proof verification to succeed.
161        let _circuit_translation_index =
162            circuit::Field::<A>::new(circuit::Mode::Public, console::types::Field::<N>::from_u16(translation_index));
163
164        // Inject the register index as `Mode::Public`.
165        let circuit_record_register_index = circuit::Field::<A>::new(
166            circuit::Mode::Public,
167            console::types::Field::<N>::from_u16(self.record_register_index),
168        );
169
170        // Inject the commitment or serial number of the non-external record (if not
171        // `is_external_record`) or the input/output ID of the external record
172        // (if `is_external_record`) as `Mode::Public`.
173        let circuit_id_static = circuit::Field::<A>::new(circuit::Mode::Public, self.id_static);
174
175        // Inject the ID of the dynamic record as `Mode::Public`.
176        let circuit_id_dynamic = circuit::Field::<A>::new(circuit::Mode::Public, self.id_dynamic);
177
178        // ******** Private inputs (including implicit constants such as record-field names)
179
180        // Inject the static record as `Mode::Private`.
181        let circuit_record_static =
182            circuit::Record::<A, circuit::Plaintext<A>>::new(circuit::Mode::Private, self.record_static.clone());
183
184        // Inject the dynamic record as `Mode::Private`.
185        let circuit_record_dynamic =
186            circuit::DynamicRecord::<A>::new(circuit::Mode::Private, self.record_dynamic.clone());
187
188        // Inject the transition view key as `Mode::Private`.
189        let circuit_tvk = circuit::Field::<A>::new(circuit::Mode::Private, self.tvk);
190
191        // Inject the record view key of the static record as `Mode::Private`.
192        // Defaults to zero if not applicable (i.e. external records).
193        let circuit_record_view_key =
194            circuit::Field::<A>::new(circuit::Mode::Private, self.record_view_key.unwrap_or_else(Field::zero));
195
196        // Inject the additional point used to produce the serial number as `Mode::Private`.
197        // Defaults to zero if not applicable (i.e. outputs or external records).
198        let circuit_gamma = circuit::Group::<A>::new(circuit::Mode::Private, self.gamma.unwrap_or_else(Group::zero));
199
200        // ******** Computing the IDs of the dynamic and static records
201
202        // Compute the ID of the dynamic record.
203        let actual_id_dynamic = compute_circuit_dynamic_or_external_record_id(
204            circuit_function_id.clone(),
205            circuit_record_dynamic.to_fields(),
206            circuit_tvk.clone(),
207            circuit_record_register_index.clone(),
208        );
209
210        let circuit_static_commitment =
211            circuit_record_static.to_commitment(&circuit_program_id, &circuit_record_name, &circuit_record_view_key);
212
213        let circuit_static_serial_number = circuit::Record::<A, circuit::Plaintext<A>>::serial_number_from_gamma(
214            &circuit_gamma,
215            circuit_static_commitment.clone(),
216        );
217
218        // Input/output ID of the static record if it is not external (serial number or commitment).
219        let actual_id_static_non_external = circuit::Field::<A>::ternary(
220            &circuit_is_to_static,
221            &circuit_static_serial_number,
222            &circuit_static_commitment,
223        );
224
225        // Input/output ID of the static record if it is external.
226        // Note: External records have the same InputID and OutputID formula.
227        let actual_id_static_external = compute_circuit_dynamic_or_external_record_id(
228            circuit_function_id,
229            circuit_record_static.to_fields(),
230            circuit_tvk,
231            circuit_record_register_index,
232        );
233
234        let actual_id_static = circuit::Field::<A>::ternary(
235            &circuit_is_external_record,
236            &actual_id_static_external,
237            &actual_id_static_non_external,
238        );
239
240        // ******** Merkleizing the static-record data
241
242        let circuit_tree = circuit::DynamicRecord::<A>::merkleize_data(circuit_record_static.data())?;
243        let circuit_data_root = circuit_tree.root();
244
245        // ******** Assertions
246
247        A::assert_eq(circuit_record_static.owner().to_group(), circuit_record_dynamic.owner().to_group())?;
248        A::assert_eq(circuit_record_static.nonce(), circuit_record_dynamic.nonce())?;
249        A::assert_eq(circuit_record_static.version(), circuit_record_dynamic.version())?;
250        A::assert_eq(circuit_data_root, circuit_record_dynamic.root())?;
251        A::assert_eq(actual_id_static, circuit_id_static)?;
252        A::assert_eq(actual_id_dynamic, circuit_id_dynamic)?;
253
254        Ok(())
255    }
256
257    /// The circuit for record-translation verification
258    ///
259    /// # Operation outline
260    /// The `[[ ]]` notation is used to denote public inputs or constants.
261    /// ```ignore
262    ///     cm = commit([[program_id]], [[record_name]], record_static, record_view_key)
263    ///     sn = serial_number(cm, gamma)
264    ///     actual_id_non_external = is_to_static ? sn : cm
265    ///     actual_id_external =  HashPSD8([[function_id]] | record_static | tvk | [[record_register_index]])
266    ///     actual_id_static = is_external ? actual_id_external : actual_id_non_external
267    ///     actual_id_dynamic = HashPSD8([[function_id]] | record_dynamic | tvk | [[record_register_index]])
268    ///
269    ///     assert record_static.owner == record_dynamic.owner
270    ///     assert record_static.nonce == record_dynamic.nonce
271    ///     assert record_static.version == record_dynamic.version
272    ///     assert merkleize(record_static) == record_dynamic.root
273    ///     assert [[id_record_static]] == actual_id_static
274    ///     assert [[id_record_dynamic]] == actual_id_dynamic
275    /// ```
276    pub fn to_circuit_assignment<A: circuit::Aleo<Network = N>>(
277        &self,
278        translation_index: u16,
279    ) -> Result<circuit::Assignment<N::Field>> {
280        self.to_circuit_assignment_internal::<A>(translation_index)?;
281        Stack::log_circuit::<A>(
282            format_args!("Translation circuit for dynamic record with nonce {}", self.record_static.nonce()),
283            "TranslationAssignment",
284        );
285        Ok(A::eject_assignment_and_reset())
286    }
287}