pessimistic_proof_core/
proof.rs

1use agglayer_bincode as bincode;
2use agglayer_primitives::{Address, Digest};
3use agglayer_tries::roots::LocalExitRoot;
4use hex_literal::hex;
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7use unified_bridge::{
8    Error, GlobalIndex, ImportedBridgeExitCommitmentValues, ImportedBridgeExitCommitmentVersion,
9    LocalExitTreeError, NetworkId, TokenInfo,
10};
11
12use crate::{
13    aggchain_data::MultisigError,
14    local_state::{
15        commitment::{
16            PessimisticRootCommitmentValues, PessimisticRootCommitmentVersion, StateCommitment,
17        },
18        NetworkState,
19    },
20    multi_batch_header::MultiBatchHeader,
21};
22
23/// Refers to the commitment on the imported bridge exits involved in the
24/// aggchain proof public values (`commit_imported_bridge_exits` field).
25/// This constant defines which commitment version is expected to verify the
26/// aggchain proof.
27pub const IMPORTED_BRIDGE_EXIT_COMMITMENT_VERSION: ImportedBridgeExitCommitmentVersion =
28    ImportedBridgeExitCommitmentVersion::V3;
29
30/// Represents all errors that can occur while generating the proof.
31///
32/// Several commitments are declared either by the chains (e.g., the local exit
33/// root) or by the agglayer (e.g., the balance and nullifier root), and are
34/// later re-computed by the prover to ensure that they match the witness data.
35/// Consequently, several errors highlight a mismatch between what is *declared*
36/// as witness and what is *computed* by the prover.
37#[derive(Clone, Error, Debug, Serialize, Deserialize, PartialEq, Eq)]
38pub enum ProofError {
39    // Note: The following arms are no longer generated but may be present in
40    //       storage having been produced by an older version of the node.
41    #[error("Invalid previous local exit root. declared: {declared}, computed: {computed}")]
42    InvalidPreviousLocalExitRoot { declared: Digest, computed: Digest },
43    #[error("Invalid previous balance root. declared: {declared}, computed: {computed}")]
44    InvalidPreviousBalanceRoot { declared: Digest, computed: Digest },
45    #[error("Invalid previous nullifier root. declared: {declared}, computed: {computed}")]
46    InvalidPreviousNullifierRoot { declared: Digest, computed: Digest },
47    #[error("Invalid new local exit root. declared: {declared}, computed: {computed}")]
48    InvalidNewLocalExitRoot { declared: Digest, computed: Digest },
49    #[error("Invalid new balance root. declared: {declared}, computed: {computed}")]
50    InvalidNewBalanceRoot { declared: Digest, computed: Digest },
51    #[error("Invalid new nullifier root. declared: {declared}, computed: {computed}")]
52    InvalidNewNullifierRoot { declared: Digest, computed: Digest },
53
54    /// The provided imported bridge exit is invalid.
55    #[error("Invalid imported bridge exit. global index: {global_index:?}, error: {source}")]
56    InvalidImportedBridgeExit {
57        source: Error,
58        global_index: GlobalIndex,
59    },
60
61    /// The commitment to the list of imported bridge exits is invalid.
62    #[error(
63        "Invalid commitment on the imported bridge exits. declared: {declared}, computed: \
64         {computed}"
65    )]
66    InvalidImportedExitsRoot { declared: Digest, computed: Digest },
67
68    // Note: No longer produced, present for storage compatibility.
69    #[error("Mismatch between the imported bridge exits list and its commitment.")]
70    MismatchImportedExitsRoot,
71
72    /// The provided nullifier path is invalid.
73    #[error("Invalid nullifier path.")]
74    InvalidNullifierPath,
75
76    /// The provided balance path is invalid.
77    #[error("Invalid balance path.")]
78    InvalidBalancePath,
79
80    /// The imported bridge exit led to balance overflow.
81    #[error("Balance overflow in bridge exit.")]
82    BalanceOverflowInBridgeExit,
83
84    /// The bridge exit led to balance underflow.
85    #[error("Balance underflow in bridge exit.")]
86    BalanceUnderflowInBridgeExit,
87
88    /// The provided bridge exit goes to the sender's own network which is not
89    /// permitted.
90    #[error("Cannot perform bridge exit to the same network as the origin.")]
91    CannotExitToSameNetwork,
92
93    /// The provided bridge exit message is invalid.
94    #[error("Invalid message origin network.")]
95    InvalidMessageOriginNetwork,
96
97    /// The token address is zero if and only if it refers to the L1 native eth.
98    #[error("Invalid L1 TokenInfo. TokenInfo: {0:?}")]
99    InvalidL1TokenInfo(TokenInfo),
100
101    /// The provided token is missing a balance proof.
102    #[error("Missing token balance proof. TokenInfo: {0:?}")]
103    MissingTokenBalanceProof(TokenInfo),
104
105    /// The provided token comes with multiple balance proofs.
106    #[error("Duplicate token in balance proofs. TokenInfo: {0:?}")]
107    DuplicateTokenBalanceProof(TokenInfo),
108
109    /// The signature on the state transition is invalid.
110    #[error("Invalid signature.")]
111    InvalidSignature,
112
113    /// The signer recovered from the signature differs from the one declared as
114    /// witness.
115    #[error("Invalid signer. declared: {declared}, recovered: {recovered}")]
116    InvalidSigner {
117        declared: Address,
118        recovered: Address,
119    },
120
121    /// The operation cannot be applied on the local exit tree.
122    #[error(transparent)]
123    InvalidLocalExitTreeOperation(#[from] LocalExitTreeError),
124
125    /// Unknown error.
126    #[error("Unknown error: {0}")]
127    Unknown(String),
128
129    /// The previous pessimistic root is not re-computable.
130    #[error(
131        "Invalid previous pessimistic root. declared: {declared}, ppr_v2: {computed_v2}, ppr_v3: \
132         {computed_v3}"
133    )]
134    InvalidPreviousPessimisticRoot {
135        declared: Digest,
136        computed_v2: Digest,
137        computed_v3: Digest,
138    },
139
140    /// The signature is on a payload that is with an inconsistent version.
141    #[error("Inconsistent signed payload version.")]
142    InconsistentSignedPayload,
143
144    /// Height overflow.
145    #[error("Height overflow")]
146    HeightOverflow,
147
148    /// Invalid multisig
149    #[error("Invalid multisig")]
150    InvalidMultisig(#[source] MultisigError),
151}
152
153/// Outputs of the pessimistic proof.
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
155pub struct PessimisticProofOutput {
156    /// The previous local exit root.
157    pub prev_local_exit_root: LocalExitRoot,
158    /// The previous pessimistic root.
159    pub prev_pessimistic_root: Digest,
160    /// The l1 info root against which we prove the inclusion of the imported
161    /// bridge exits.
162    pub l1_info_root: Digest,
163    /// The origin network of the pessimistic proof.
164    pub origin_network: NetworkId,
165    /// The aggchain hash.
166    pub aggchain_hash: Digest,
167    /// The new local exit root.
168    pub new_local_exit_root: LocalExitRoot,
169    /// The new pessimistic root.
170    pub new_pessimistic_root: Digest,
171}
172
173impl PessimisticProofOutput {
174    pub fn bincode_codec() -> bincode::Codec<impl bincode::Options> {
175        bincode::contracts()
176    }
177}
178
179pub const EMPTY_LER: LocalExitRoot = LocalExitRoot::new(Digest(hex!(
180    "27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757"
181)));
182
183pub const EMPTY_PP_ROOT_V2: Digest = Digest(hex!(
184    "c89c9c0f2ebd19afa9e5910097c43e56fb4aff3a06ddee8d7c9bae09bc769184"
185));
186
187/// Represents all the enforced values for the stark and for the signed
188/// commitments.
189#[derive(Clone)]
190pub struct ConstrainedValues {
191    pub initial_state_commitment: StateCommitment,
192    pub final_state_commitment: StateCommitment,
193    pub prev_pessimistic_root: Digest,
194    pub prev_pessimistic_root_version: PessimisticRootCommitmentVersion,
195    pub height: u64,
196    pub origin_network: NetworkId,
197    pub l1_info_root: Digest,
198    pub commit_imported_bridge_exits: ImportedBridgeExitCommitmentValues,
199    pub certificate_id: Digest,
200}
201
202impl ConstrainedValues {
203    fn try_new(
204        batch_header: &MultiBatchHeader,
205        initial_state_commitment: &StateCommitment,
206        final_state_commitment: &StateCommitment,
207    ) -> Result<Self, ProofError> {
208        let settled_prev_pp_root = batch_header.prev_pessimistic_root;
209
210        // Infer the version of the settled prev pp root based on the constrained
211        // values. Return error if unable to re-compute a matching prev pp root.
212        let prev_pessimistic_root_version = PessimisticRootCommitmentValues {
213            balance_root: initial_state_commitment.balance_root,
214            nullifier_root: initial_state_commitment.nullifier_root,
215            ler_leaf_count: initial_state_commitment.ler_leaf_count,
216            height: batch_header.height,
217            origin_network: batch_header.origin_network,
218        }
219        .infer_settled_pp_root_version(settled_prev_pp_root)?;
220
221        Ok(Self {
222            initial_state_commitment: initial_state_commitment.clone(),
223            final_state_commitment: final_state_commitment.clone(),
224            height: batch_header.height,
225            origin_network: batch_header.origin_network,
226            l1_info_root: batch_header.l1_info_root,
227            commit_imported_bridge_exits: batch_header.commit_imported_bridge_exits(),
228            certificate_id: batch_header.certificate_id,
229            prev_pessimistic_root: settled_prev_pp_root,
230            prev_pessimistic_root_version,
231        })
232    }
233}
234
235/// Proves that the given [`MultiBatchHeader`] can be applied on the given
236/// [`NetworkState`].
237pub fn generate_pessimistic_proof(
238    initial_network_state: NetworkState,
239    batch_header: &MultiBatchHeader,
240) -> Result<(PessimisticProofOutput, StateCommitment), ProofError> {
241    // Get the initial state commitment
242    let initial_state_commitment = initial_network_state.get_state_commitment();
243    let mut network_state: NetworkState = initial_network_state;
244    let final_state_commitment = network_state.apply_batch_header(batch_header)?;
245
246    // Also verify initial state commitment and PP root matches
247    let constrained_values = ConstrainedValues::try_new(
248        batch_header,
249        &initial_state_commitment,
250        &final_state_commitment,
251    )?;
252
253    // Verify multisig, aggchain proof, or both.
254    let target_pp_root_version = batch_header.aggchain_data.verify(constrained_values)?;
255
256    let height = batch_header
257        .height
258        .checked_add(1)
259        .ok_or(ProofError::HeightOverflow)?;
260
261    let new_pessimistic_root = PessimisticRootCommitmentValues {
262        balance_root: final_state_commitment.balance_root,
263        nullifier_root: final_state_commitment.nullifier_root,
264        ler_leaf_count: final_state_commitment.ler_leaf_count,
265        height,
266        origin_network: batch_header.origin_network,
267    }
268    .compute_pp_root(target_pp_root_version);
269
270    Ok((
271        PessimisticProofOutput {
272            prev_local_exit_root: zero_if_empty_local_exit_root(initial_state_commitment.exit_root),
273            prev_pessimistic_root: batch_header.prev_pessimistic_root,
274            l1_info_root: batch_header.l1_info_root,
275            origin_network: batch_header.origin_network,
276            aggchain_hash: batch_header.aggchain_data.aggchain_hash(),
277            new_local_exit_root: zero_if_empty_local_exit_root(final_state_commitment.exit_root),
278            new_pessimistic_root,
279        },
280        final_state_commitment,
281    ))
282}
283
284// NOTE: Hack to comply with the L1 contracts which assume `0x00..00` for the
285// empty roots of the different trees involved. Therefore, we do
286// one mapping of empty tree hash <> 0x00..0 on the public inputs.
287pub fn zero_if_empty_local_exit_root(root: LocalExitRoot) -> LocalExitRoot {
288    if root == EMPTY_LER {
289        LocalExitRoot::default()
290    } else {
291        root
292    }
293}