//! Arcis implementation of <https://github.com/solana-program/zk-elgamal-proof/blob/main/zk-sdk/src/sigma_proofs/percentage_with_cap.rs>

use crate::{
    core::{
        circuits::boolean::{boolean_value::BooleanValue, byte::Byte},
        global_value::{
            curve_value::{CompressedCurveValue, CurveValue},
            value::FieldValue,
        },
    },
    traits::{Reveal, ToLeBytes},
    utils::{
        field::ScalarField,
        zkp::{
            pedersen::{PedersenCommitment, PedersenOpening},
            transcript::Transcript,
            util::UNIT_LEN,
        },
    },
};
use std::sync::LazyLock;
use zk_elgamal_proof::encryption::{pedersen::H, PEDERSEN_COMMITMENT_LEN};

const PERCENTAGE_WITH_CAP_PROOF_LEN: usize = UNIT_LEN * 8;

const PERCENTAGE_WITH_CAP_PROOF_CONTEXT_LEN: usize = 3 * PEDERSEN_COMMITMENT_LEN + 8;

pub const PERCENTAGE_WITH_CAP_PROOF_DATA_LEN: usize =
    PERCENTAGE_WITH_CAP_PROOF_CONTEXT_LEN + PERCENTAGE_WITH_CAP_PROOF_LEN;

/// The instruction data that is needed for the `ProofInstruction::VerifyPercentageWithCap`
/// instruction.
///
/// It includes the cryptographic proof as well as the context data information needed to verify
/// the proof.
#[derive(Clone, Copy)]
pub struct PercentageWithCapProofData {
    pub context: PercentageWithCapProofContext,
    pub proof: PercentageWithCapProof,
}

/// The context data needed to verify a percentage-with-cap proof.
#[derive(Clone, Copy)]
pub struct PercentageWithCapProofContext {
    /// The Pedersen commitment to the percentage amount.
    pub percentage_commitment: PedersenCommitment,
    /// The Pedersen commitment to the delta amount.
    pub delta_commitment: PedersenCommitment,
    /// The Pedersen commitment to the claimed amount.
    pub claimed_commitment: PedersenCommitment,
    /// The maximum cap bound.
    pub max_value: FieldValue<ScalarField>,
}

impl PercentageWithCapProofContext {
    fn new_transcript(&self) -> Transcript<BooleanValue> {
        let mut transcript = Transcript::new(b"percentage-with-cap-instruction");
        transcript.append_point(
            b"percentage-commitment",
            &self.percentage_commitment.get_point().compress(),
        );
        transcript.append_point(
            b"delta-commitment",
            &self.delta_commitment.get_point().compress(),
        );
        transcript.append_point(
            b"claimed-commitment",
            &self.claimed_commitment.get_point().compress(),
        );
        // same as append_u64 but for FieldValues
        transcript.append_message(b"max-value", &self.max_value.to_le_bytes()[..8]);
        transcript
    }

    pub fn to_bytes(&self) -> [Byte<BooleanValue>; PERCENTAGE_WITH_CAP_PROOF_CONTEXT_LEN] {
        let mut bytes = [Byte::<BooleanValue>::from(0); PERCENTAGE_WITH_CAP_PROOF_CONTEXT_LEN];
        bytes[..PEDERSEN_COMMITMENT_LEN]
            .copy_from_slice(&self.percentage_commitment.get_point().compress().to_bytes());
        let mut offset = PEDERSEN_COMMITMENT_LEN;
        bytes[offset..offset + PEDERSEN_COMMITMENT_LEN]
            .copy_from_slice(&self.delta_commitment.get_point().compress().to_bytes());
        offset += PEDERSEN_COMMITMENT_LEN;
        bytes[offset..offset + PEDERSEN_COMMITMENT_LEN]
            .copy_from_slice(&self.claimed_commitment.get_point().compress().to_bytes());
        offset += PEDERSEN_COMMITMENT_LEN;
        bytes[offset..offset + 8].copy_from_slice(&self.max_value.to_le_bytes()[..8]);
        bytes
    }
}

impl PercentageWithCapProofData {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        percentage_commitment: &PedersenCommitment,
        percentage_opening: &PedersenOpening,
        percentage_amount: FieldValue<ScalarField>,
        delta_commitment: &PedersenCommitment,
        delta_opening: &PedersenOpening,
        delta_amount: FieldValue<ScalarField>,
        claimed_commitment: &PedersenCommitment,
        claimed_opening: &PedersenOpening,
        max_value: FieldValue<ScalarField>,
    ) -> Self {
        let context = PercentageWithCapProofContext {
            percentage_commitment: *percentage_commitment,
            delta_commitment: *delta_commitment,
            claimed_commitment: *claimed_commitment,
            max_value,
        };
        let mut transcript = context.new_transcript();
        let proof = PercentageWithCapProof::new(
            percentage_commitment,
            percentage_opening,
            percentage_amount,
            delta_commitment,
            delta_opening,
            delta_amount,
            claimed_commitment,
            claimed_opening,
            max_value,
            &mut transcript,
        );
        PercentageWithCapProofData { context, proof }
    }

    pub fn to_bytes(&self) -> [Byte<BooleanValue>; PERCENTAGE_WITH_CAP_PROOF_DATA_LEN] {
        let mut bytes = [Byte::<BooleanValue>::from(0); PERCENTAGE_WITH_CAP_PROOF_DATA_LEN];
        bytes[..PERCENTAGE_WITH_CAP_PROOF_CONTEXT_LEN].copy_from_slice(&self.context.to_bytes());
        bytes[PERCENTAGE_WITH_CAP_PROOF_CONTEXT_LEN..].copy_from_slice(&self.proof.to_bytes());
        bytes
    }
}

/// Percentage-with-cap proof.
///
/// The proof consists of two main components: `percentage_max_proof` and
/// `percentage_equality_proof`. If the committed amount is greater than the maximum cap value,
/// then the `percentage_max_proof` is properly generated and `percentage_equality_proof` is
/// simulated. If the encrypted amount is smaller than the maximum cap bound, the
/// `percentage_equality_proof` is properly generated and `percentage_max_proof` is simulated.
#[allow(non_snake_case, dead_code)]
#[derive(Clone, Copy)]
pub struct PercentageWithCapProof {
    /// Proof that the committed amount equals the maximum cap bound.
    percentage_max_proof: PercentageMaxProof,
    /// Proof that the `delta` value is equal to the `claimed` value.
    percentage_equality_proof: PercentageEqualityProof,
}

#[allow(non_snake_case, dead_code)]
impl PercentageWithCapProof {
    /// Creates a percentage-with-cap sigma proof.
    ///
    /// A typical percentage-with-cap application is defined with respect to the following values:
    /// - a commitment encoding a `transfer_amount` and a commitment encoding a `percentage_amount`
    /// - two constants `percentage_rate_basis_point`, which defines the percentage rate in units of
    ///   0.01% and `max_value`, which defines the max cap amount.
    ///
    /// This setting requires that the `percentage_amount` is either a certain percentage of the
    /// `transfer_amount` (determined by the `percentage_rate_basis_point`) or is equal to the max
    /// cap amount (determined by `max_value`).
    ///
    /// If `percentage_amount < max_value`, then assuming that there is no division rounding, the
    /// `percentage_amount` must satisfy the relation `(transfer_amount *
    /// percentage_rate_basis_point) / 10_000 = percentage_amount` or equivalently,
    /// `(transfer_amount * percentage_rate_basis_point) - (10_000 * percentage_amount) = 0`. More
    /// generally, let `delta_amount = (transfer_amount * percentage_rate_basis_point) - (10_000
    /// * percentage_amount)`. Then assuming that a division rounding could occur, the
    /// `delta_amount` must satisfy the bound `0 <= delta_amount < 10_000`.
    ///
    /// If `percentage_amount >= max_amount`, then `percentage_amount = max_value` and therefore,
    /// the prover can generate a proof certifying that a percentage commitment exactly encodes
    /// `max_value`. If `percentage_amount < max_value`, then the prover can create a commitment
    /// (referred to as the `claimed_amount`) to `delta_amount` and create a range proof certifying
    /// that the committed value satisfies the bound `0 <= delta_amount < 10_000`.
    ///
    /// Since the type of proof that a prover generates reveals information about the transfer and
    /// percentage amounts, the prover must generate and include both types of proofs. If
    /// `percentage_amount >= max_value`, then the prover generates a valid `percentage_max_proof`,
    /// but commits to 0 as the `claimed_amount` and simulates ("fakes") a proof
    /// (`percentage_equality_proof`) that this is valid. If `percentage_amount > max_value`, then
    /// the prover simulates a `percentage_max_proof`, and creates a valid
    /// `percentage_equality_proof` certifying that the claimed delta value is equal to the "real"
    /// delta value.
    ///
    /// * `percentage_commitment` - The Pedersen commitment to a percentage amount
    /// * `percentage_opening` - The Pedersen opening of a percentage amount
    /// * `percentage_amount` - The percentage amount
    /// * `delta_commitment` - The Pedersen commitment to a delta amount
    /// * `delta_opening` - The Pedersen opening of a delta amount
    /// * `delta_amount` - The delta amount
    /// * `claimed_commitment` - The Pedersen commitment to a claimed amount
    /// * `claimed_opening` - The Pedersen opening of a claimed amount
    /// * `max_value` - The maximum cap bound
    /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        percentage_commitment: &PedersenCommitment,
        percentage_opening: &PedersenOpening,
        percentage_amount: FieldValue<ScalarField>,
        delta_commitment: &PedersenCommitment,
        delta_opening: &PedersenOpening,
        delta_amount: FieldValue<ScalarField>,
        claimed_commitment: &PedersenCommitment,
        claimed_opening: &PedersenOpening,
        max_value: FieldValue<ScalarField>,
        transcript: &mut Transcript<BooleanValue>,
    ) -> Self {
        use crate::traits::{GreaterThan, Select};

        transcript.percentage_with_cap_proof_domain_separator();

        // The strategy is a little different from the plaintext case where one would run both the
        // above max case (simulate equality proof and generate max proof) and the below max case
        // (simulate max proof and generate equality proof) and then ct-select based on below_max =
        // max_value > percentage_amount. If one did the same in MPC, then in order to keep
        // below_max secret, both proofs would need to be generated in full MPC, whereas for all
        // other proofs we can reveal everything that gets hashed into the transcript. This would
        // lead to a huge performace drop. What we do instead is to simulate both proofs, perform
        // the first part of the non-simulated proofs (everything that can be done before invoking
        // the transcript), then obliviously select and reveal the data that gets hashed into the
        // transcript. And then do the remaining work, again obliviously select and reveal data that
        // makes up the returned proof.

        // 1. above max scenario: simulate equality proof and generate first part of max proof
        // simulate equality proof
        let C_delta = delta_commitment.get_point();
        let C_claimed = claimed_commitment.get_point();

        let z_x_above_max = FieldValue::<ScalarField>::random();
        let z_delta_above_max = FieldValue::<ScalarField>::random();
        let z_claimed_above_max = FieldValue::<ScalarField>::random();
        let c_equality_above_max = FieldValue::<ScalarField>::random();

        let Y_delta_above_max = CurveValue::multiscalar_mul(
            vec![z_x_above_max, z_delta_above_max, -c_equality_above_max],
            vec![
                CurveValue::generator(),
                CurveValue::from(*LazyLock::force(&H)),
                *C_delta,
            ],
        );

        let Y_claimed_above_max = CurveValue::multiscalar_mul(
            vec![z_x_above_max, z_claimed_above_max, -c_equality_above_max],
            vec![
                CurveValue::generator(),
                CurveValue::from(*LazyLock::force(&H)),
                *C_claimed,
            ],
        );

        // generate first part of max proof
        let r_percentage = percentage_opening.get_scalar();

        let y_max_proof = FieldValue::<ScalarField>::random();
        let Y_max_proof_above_max = y_max_proof * CurveValue::from(*LazyLock::force(&H));

        // 2. below max scenario: simulate max proof and generate first part of equality proof
        // simulate max proof
        let m = max_value;
        let C_percentage = percentage_commitment.get_point();

        let z_max_proof_below_max = FieldValue::<ScalarField>::random();
        let c_max_proof_below_max = FieldValue::<ScalarField>::random(); // random challenge

        // solve for Y_max in the verification algebraic relation
        let Y_max_proof_below_max = CurveValue::multiscalar_mul(
            vec![
                z_max_proof_below_max,
                -c_max_proof_below_max,
                c_max_proof_below_max * m,
            ],
            vec![
                CurveValue::from(*LazyLock::force(&H)),
                *C_percentage,
                CurveValue::generator(),
            ],
        );

        // generate first part of equality proof
        let x = delta_amount;

        let r_delta = delta_opening.get_scalar();
        let r_claimed = claimed_opening.get_scalar();

        let y_x = FieldValue::<ScalarField>::random();
        let y_delta = FieldValue::<ScalarField>::random();
        let y_claimed = FieldValue::<ScalarField>::random();

        let Y_delta_below_max = CurveValue::multiscalar_mul(
            vec![y_x, y_delta],
            vec![
                CurveValue::generator(),
                CurveValue::from(*LazyLock::force(&H)),
            ],
        );
        let Y_claimed_below_max = CurveValue::multiscalar_mul(
            vec![y_x, y_claimed],
            vec![
                CurveValue::generator(),
                CurveValue::from(*LazyLock::force(&H)),
            ],
        );

        // 3. compute below_max = max_value > percentage_amount and obliviously select
        let below_max = max_value.gt(percentage_amount);

        let Y_max_proof = below_max
            .select(Y_max_proof_below_max, Y_max_proof_above_max)
            .reveal()
            .compress();
        let Y_delta = below_max
            .select(Y_delta_below_max, Y_delta_above_max)
            .reveal()
            .compress();
        let Y_claimed = below_max
            .select(Y_claimed_below_max, Y_claimed_above_max)
            .reveal()
            .compress();

        // 4. hash the compressed points into the transcript
        transcript.append_point(b"Y_max_proof", &Y_max_proof);
        transcript.append_point(b"Y_delta", &Y_delta);
        transcript.append_point(b"Y_claimed", &Y_claimed);

        // challenge scalars
        let c = transcript.challenge_scalar(b"c");
        transcript.challenge_scalar(b"w");

        // 5. above max scenario
        // generate second part of max proof
        let c_max_proof_above_max = c - c_equality_above_max;
        let z_max_proof_above_max = c_max_proof_above_max * *r_percentage + y_max_proof;

        // obliviously select
        let z_max_proof = below_max
            .select(z_max_proof_below_max, z_max_proof_above_max)
            .reveal();
        let c_max_proof = below_max
            .select(c_max_proof_below_max, c_max_proof_above_max)
            .reveal();

        let percentage_max_proof = PercentageMaxProof {
            Y_max_proof,
            z_max_proof,
            c_max_proof,
        };

        // 6. below max scenario
        // generate second part of equality proof
        let c_equality_below_max = c - c_max_proof_below_max;
        let z_x_below_max = c_equality_below_max * x + y_x;
        let z_delta_below_max = c_equality_below_max * *r_delta + y_delta;
        let z_claimed_below_max = c_equality_below_max * *r_claimed + y_claimed;

        // obliviously select
        let z_x = below_max.select(z_x_below_max, z_x_above_max).reveal();
        let z_delta = below_max
            .select(z_delta_below_max, z_delta_above_max)
            .reveal();
        let z_claimed = below_max
            .select(z_claimed_below_max, z_claimed_above_max)
            .reveal();

        let percentage_equality_proof = PercentageEqualityProof {
            Y_delta,
            Y_claimed,
            z_x,
            z_delta,
            z_claimed,
        };

        Self {
            percentage_max_proof,
            percentage_equality_proof,
        }
    }

    pub fn to_bytes(&self) -> [Byte<BooleanValue>; PERCENTAGE_WITH_CAP_PROOF_LEN] {
        let mut buf = [Byte::<BooleanValue>::from(0); PERCENTAGE_WITH_CAP_PROOF_LEN];
        let mut chunks = buf.chunks_mut(UNIT_LEN);
        chunks
            .next()
            .unwrap()
            .copy_from_slice(&self.percentage_max_proof.Y_max_proof.to_bytes());
        chunks
            .next()
            .unwrap()
            .copy_from_slice(&self.percentage_max_proof.z_max_proof.to_le_bytes());
        chunks
            .next()
            .unwrap()
            .copy_from_slice(&self.percentage_max_proof.c_max_proof.to_le_bytes());
        chunks
            .next()
            .unwrap()
            .copy_from_slice(&self.percentage_equality_proof.Y_delta.to_bytes());
        chunks
            .next()
            .unwrap()
            .copy_from_slice(&self.percentage_equality_proof.Y_claimed.to_bytes());
        chunks
            .next()
            .unwrap()
            .copy_from_slice(&self.percentage_equality_proof.z_x.to_le_bytes());
        chunks
            .next()
            .unwrap()
            .copy_from_slice(&self.percentage_equality_proof.z_delta.to_le_bytes());
        chunks
            .next()
            .unwrap()
            .copy_from_slice(&self.percentage_equality_proof.z_claimed.to_le_bytes());
        buf
    }
}

/// The percentage max proof.
///
/// The proof certifies that a Pedersen commitment encodes the maximum cap bound.
#[allow(non_snake_case)]
#[derive(Clone, Copy)]
pub struct PercentageMaxProof {
    Y_max_proof: CompressedCurveValue,
    z_max_proof: FieldValue<ScalarField>,
    c_max_proof: FieldValue<ScalarField>,
}

/// The percentage equality proof.
///
/// The proof certifies that the "real" delta value commitment and the "claimed" delta value
/// commitment encode the same message.
#[allow(non_snake_case)]
#[derive(Clone, Copy)]
pub struct PercentageEqualityProof {
    Y_delta: CompressedCurveValue,
    Y_claimed: CompressedCurveValue,
    z_x: FieldValue<ScalarField>,
    z_delta: FieldValue<ScalarField>,
    z_claimed: FieldValue<ScalarField>,
}

#[cfg(test)]
mod tests {
    use crate::{
        core::{
            bounds::FieldBounds,
            expressions::{
                curve_expr::{CurveExpr, InputInfo},
                domain::Domain,
                expr::EvalValue,
                field_expr::FieldExpr,
                InputKind,
            },
            global_value::{
                curve_value::CurveValue,
                global_expr_store::with_local_expr_store_as_global,
                value::FieldValue,
            },
            ir_builder::{ExprStore, IRBuilder},
        },
        utils::{
            curve_point::CurvePoint,
            field::ScalarField,
            used_field::UsedField,
            zkp::{
                pedersen::{PedersenCommitment, PedersenOpening},
                percentage_with_cap::PercentageWithCapProofData,
            },
        },
    };
    use curve25519_dalek::Scalar;
    use group::GroupEncoding;
    use primitives::algebra::elliptic_curve::{Curve as AsyncMPCCurve, Curve25519Ristretto};
    use rand::{Rng, RngCore};
    use rand_chacha::ChaCha12Rng;
    use rustc_hash::FxHashMap;
    use std::rc::Rc;
    use zk_elgamal_proof::{
        encryption::pedersen::{
            Pedersen as SolanaPedersen,
            PedersenCommitment as SolanaPedersenCommitment,
            PedersenOpening as SolanaPedersenOpening,
        },
        zk_elgamal_proof_program::proof_data::{
            PercentageWithCapProofData as SolanaPercentageWithCapProofData,
            ZkProofData,
        },
    };

    #[test]
    #[allow(non_snake_case)]
    fn test_percentage_with_cap_above_max_proof() {
        let rng = &mut crate::utils::test_rng::get();

        // any value between 0.01% and 100%
        let percentage_rate = (rng.next_u64() % 10000_u64) + 1;
        // the division by percentage_rate serves only to avoid overflows in the multiplication
        // below
        let transfer_amount = rng.next_u64() / percentage_rate;
        let percentage_amount = (transfer_amount * percentage_rate) / 10000_u64;
        let max_value = rng.next_u64() % percentage_amount;
        let delta_amount: u64 = (transfer_amount * percentage_rate) % 10000_u64;

        let (transfer_commitment, transfer_opening) = SolanaPedersen::new(transfer_amount);
        let (mut percentage_commitment, percentage_opening) = SolanaPedersen::new(max_value);

        // we flip a coin and generate an invalid proof if the bit is false
        let is_valid_proof = rng.gen_bool(0.5);
        if !is_valid_proof {
            percentage_commitment = percentage_commitment - SolanaPedersen::encode(1_u64);
        }

        let scalar_rate = Scalar::from(percentage_rate);
        let delta_commitment =
            transfer_commitment * scalar_rate - percentage_commitment * Scalar::from(10000_u64);
        let delta_opening =
            transfer_opening * scalar_rate - &percentage_opening * Scalar::from(10000_u64);

        let (claimed_commitment, claimed_opening) = SolanaPedersen::new(0_u64);

        let proof_data = SolanaPercentageWithCapProofData::new(
            &percentage_commitment,
            &percentage_opening,
            percentage_amount,
            &delta_commitment,
            &delta_opening,
            delta_amount,
            &claimed_commitment,
            &claimed_opening,
            max_value,
        )
        .unwrap();

        assert_eq!(proof_data.verify_proof().is_ok(), is_valid_proof);

        let arcis_proof_data = gen_arcis_proof(
            rng,
            &percentage_commitment,
            &percentage_opening,
            percentage_amount,
            &delta_commitment,
            &delta_opening,
            delta_amount,
            &claimed_commitment,
            &claimed_opening,
            max_value,
        );

        let arcis_verification = arcis_proof_data.verify_proof();

        assert_eq!(arcis_verification.is_ok(), is_valid_proof);
    }

    #[test]
    #[allow(non_snake_case)]
    fn test_percentage_with_cap_below_max_proof() {
        let rng = &mut crate::utils::test_rng::get();

        // any value between 0.01% and 100%
        let percentage_rate = (rng.next_u64() % 10000_u64) + 1;
        // the division by percentage_rate serves only to avoid overflows in the multiplication
        // below
        let transfer_amount = rng.next_u64() / percentage_rate;
        let percentage_amount = (transfer_amount * percentage_rate) / 10000_u64;
        let max_value = rng.next_u64().max(percentage_amount + 1);
        let delta_amount: u64 = (transfer_amount * percentage_rate) % 10000_u64;

        let (transfer_commitment, transfer_opening) = SolanaPedersen::new(transfer_amount);
        let (mut percentage_commitment, percentage_opening) =
            SolanaPedersen::new(percentage_amount);

        // we flip a coin and generate an invalid proof if the bit is false
        let is_valid_proof = rng.gen_bool(0.5);
        if !is_valid_proof {
            percentage_commitment = percentage_commitment - SolanaPedersen::encode(1_u64);
        }

        let scalar_rate = Scalar::from(percentage_rate);
        let delta_commitment =
            transfer_commitment * scalar_rate - percentage_commitment * Scalar::from(10000_u64);
        let delta_opening =
            transfer_opening * scalar_rate - &percentage_opening * Scalar::from(10000_u64);

        let (claimed_commitment, claimed_opening) = SolanaPedersen::new(delta_amount);

        let proof_data = SolanaPercentageWithCapProofData::new(
            &percentage_commitment,
            &percentage_opening,
            percentage_amount,
            &delta_commitment,
            &delta_opening,
            delta_amount,
            &claimed_commitment,
            &claimed_opening,
            max_value,
        )
        .unwrap();

        assert_eq!(proof_data.verify_proof().is_ok(), is_valid_proof);

        let arcis_proof_data = gen_arcis_proof(
            rng,
            &percentage_commitment,
            &percentage_opening,
            percentage_amount,
            &delta_commitment,
            &delta_opening,
            delta_amount,
            &claimed_commitment,
            &claimed_opening,
            max_value,
        );

        let arcis_verification = arcis_proof_data.verify_proof();

        assert_eq!(arcis_verification.is_ok(), is_valid_proof);
    }

    #[allow(clippy::too_many_arguments)]
    fn gen_arcis_proof(
        rng: &mut ChaCha12Rng,
        percentage_commitment: &SolanaPedersenCommitment,
        percentage_opening: &SolanaPedersenOpening,
        percentage_amount: u64,
        delta_commitment: &SolanaPedersenCommitment,
        delta_opening: &SolanaPedersenOpening,
        delta_amount: u64,
        claimed_commitment: &SolanaPedersenCommitment,
        claimed_opening: &SolanaPedersenOpening,
        max_value: u64,
    ) -> SolanaPercentageWithCapProofData {
        let mut expr_store = IRBuilder::new(true);

        // add inputs
        let mut input_vals = FxHashMap::<usize, EvalValue>::default();
        // percentage
        let _ = <IRBuilder as ExprStore<ScalarField>>::push_curve(
            &mut expr_store,
            CurveExpr::Input(0, Rc::new(InputInfo::from(InputKind::Plaintext))),
        );
        input_vals.insert(
            0,
            EvalValue::Curve(CurvePoint::new(
                <Curve25519Ristretto as AsyncMPCCurve>::Point::from_bytes(
                    &percentage_commitment.get_point().to_bytes(),
                )
                .unwrap(),
            )),
        );
        let _ = expr_store.push_field(FieldExpr::Input(
            1,
            FieldBounds::<ScalarField>::All.as_input_info(InputKind::Secret),
        ));
        input_vals.insert(
            1,
            EvalValue::Scalar(ScalarField::from(*percentage_opening.get_scalar())),
        );
        let _ = expr_store.push_field(FieldExpr::Input(
            2,
            FieldBounds::new(
                ScalarField::from(0),
                ScalarField::power_of_two(64) - ScalarField::from(1),
            )
            .as_input_info(InputKind::Secret),
        ));
        input_vals.insert(2, EvalValue::Scalar(ScalarField::from(percentage_amount)));
        // delta
        let _ = <IRBuilder as ExprStore<ScalarField>>::push_curve(
            &mut expr_store,
            CurveExpr::Input(3, Rc::new(InputInfo::from(InputKind::Plaintext))),
        );
        input_vals.insert(
            3,
            EvalValue::Curve(CurvePoint::new(
                <Curve25519Ristretto as AsyncMPCCurve>::Point::from_bytes(
                    &delta_commitment.get_point().to_bytes(),
                )
                .unwrap(),
            )),
        );
        let _ = expr_store.push_field(FieldExpr::Input(
            4,
            FieldBounds::<ScalarField>::All.as_input_info(InputKind::Secret),
        ));
        input_vals.insert(
            4,
            EvalValue::Scalar(ScalarField::from(*delta_opening.get_scalar())),
        );
        let _ = expr_store.push_field(FieldExpr::Input(
            5,
            FieldBounds::new(
                ScalarField::from(0),
                ScalarField::power_of_two(64) - ScalarField::from(1),
            )
            .as_input_info(InputKind::Secret),
        ));
        input_vals.insert(5, EvalValue::Scalar(ScalarField::from(delta_amount)));
        // claimed
        let _ = <IRBuilder as ExprStore<ScalarField>>::push_curve(
            &mut expr_store,
            CurveExpr::Input(6, Rc::new(InputInfo::from(InputKind::Plaintext))),
        );
        input_vals.insert(
            6,
            EvalValue::Curve(CurvePoint::new(
                <Curve25519Ristretto as AsyncMPCCurve>::Point::from_bytes(
                    &claimed_commitment.get_point().to_bytes(),
                )
                .unwrap(),
            )),
        );
        let _ = expr_store.push_field(FieldExpr::Input(
            7,
            FieldBounds::<ScalarField>::All.as_input_info(InputKind::Secret),
        ));
        input_vals.insert(
            7,
            EvalValue::Scalar(ScalarField::from(*claimed_opening.get_scalar())),
        );
        // max_value
        let _ = expr_store.push_field(FieldExpr::Input(
            8,
            FieldBounds::new(
                ScalarField::from(0),
                ScalarField::power_of_two(64) - ScalarField::from(1),
            )
            .as_input_info(InputKind::Secret),
        ));
        input_vals.insert(8, EvalValue::Scalar(ScalarField::from(max_value)));

        let outputs = with_local_expr_store_as_global(
            || {
                let percentage_commitment = CurveValue::new(0);
                let percentage_opening = FieldValue::<ScalarField>::from_id(1);
                let percentage_amount = FieldValue::<ScalarField>::from_id(2);
                let delta_commitment = CurveValue::new(3);
                let delta_opening = FieldValue::<ScalarField>::from_id(4);
                let delta_amount = FieldValue::<ScalarField>::from_id(5);
                let claimed_commitment = CurveValue::new(6);
                let claimed_opening = FieldValue::<ScalarField>::from_id(7);
                let max_value = FieldValue::<ScalarField>::from_id(8);

                let arcis_proof_data = PercentageWithCapProofData::new(
                    &PedersenCommitment::new(percentage_commitment),
                    &PedersenOpening::new(percentage_opening),
                    percentage_amount,
                    &PedersenCommitment::new(delta_commitment),
                    &PedersenOpening::new(delta_opening),
                    delta_amount,
                    &PedersenCommitment::new(claimed_commitment),
                    &PedersenOpening::new(claimed_opening),
                    max_value,
                );

                arcis_proof_data
                    .to_bytes()
                    .into_iter()
                    .map(|byte| FieldValue::<ScalarField>::from(byte).get_id())
                    .collect::<Vec<usize>>()
            },
            &mut expr_store,
        );

        let ir = expr_store.into_ir(outputs);
        let result = ir
            .eval(rng, &mut input_vals)
            .map(|x| {
                x.into_iter()
                    .map(ScalarField::unwrap)
                    .collect::<Vec<ScalarField>>()
            })
            .unwrap();

        let arcis_proof_data_bytes = result
            .iter()
            .map(|byte| byte.to_le_bytes()[0])
            .collect::<Vec<u8>>();

        SolanaPercentageWithCapProofData::from_bytes(&arcis_proof_data_bytes).unwrap()
    }
}
