"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Prover = void 0;
/* eslint-disable no-console, no-await-in-loop */
const core_1 = require("@maci-protocol/core");
const crypto_1 = require("@maci-protocol/crypto");
const domainobjs_1 = require("@maci-protocol/domainobjs");
const logger_1 = require("../../ts/logger");
const utils_1 = require("../../ts/utils");
/**
 * Prover class is designed to prove message processing and tally proofs on-chain.
 */
class Prover {
    /**
     * Initialize class properties
     *
     * @param {IProverParams} params - constructor params
     */
    constructor({ pollContract, messageProcessorContract, maciContract, verifyingKeysRegistryContract, verifierContract, tallyContract, }) {
        this.pollContract = pollContract;
        this.messageProcessorContract = messageProcessorContract;
        this.maciContract = maciContract;
        this.verifyingKeysRegistryContract = verifyingKeysRegistryContract;
        this.verifierContract = verifierContract;
        this.tallyContract = tallyContract;
    }
    /**
     * Prove message processing on-chain
     *
     * @param proofs - proofs
     */
    async proveMessageProcessing(proofs) {
        // retrieve the values we need from the smart contracts
        const [treeDepths, messageBatchSize, totalSignupsAndMessages, totalBatchesProcessed, stateTreeDepth, coordinatorPublicKeyHash, mode, batchHashes, lastChainHash,] = await Promise.all([
            this.pollContract.treeDepths(),
            this.pollContract.messageBatchSize().then(Number),
            this.pollContract.totalSignupsAndMessages(),
            this.messageProcessorContract.totalBatchesProcessed().then(Number),
            this.maciContract.stateTreeDepth().then(Number),
            this.pollContract.coordinatorPublicKeyHash(),
            this.messageProcessorContract.mode(),
            this.pollContract.getBatchHashes().then((res) => [...res]),
            this.pollContract.chainHash(),
        ]);
        const numMessages = Number(totalSignupsAndMessages[1]);
        const totalMessageBatches = batchHashes.length;
        let numberBatchesProcessed = totalBatchesProcessed;
        const onChainProcessVerifyingKey = await this.verifyingKeysRegistryContract.getProcessVerifyingKey(stateTreeDepth, treeDepths.voteOptionTreeDepth, messageBatchSize, mode);
        if (numberBatchesProcessed < totalMessageBatches) {
            (0, logger_1.logMagenta)({ text: (0, logger_1.info)("Submitting proofs of message processing...") });
        }
        // process all batches left
        for (let i = numberBatchesProcessed; i < totalMessageBatches; i += 1) {
            let currentMessageBatchIndex;
            if (numberBatchesProcessed === 0) {
                const chainHash = lastChainHash;
                if (numMessages % messageBatchSize !== 0) {
                    batchHashes.push(chainHash);
                }
                currentMessageBatchIndex = batchHashes.length;
                if (currentMessageBatchIndex > 0) {
                    currentMessageBatchIndex -= 1;
                }
            }
            else {
                currentMessageBatchIndex = (totalMessageBatches - numberBatchesProcessed) * messageBatchSize;
            }
            if (numberBatchesProcessed > 0 && currentMessageBatchIndex > 0) {
                currentMessageBatchIndex /= messageBatchSize;
            }
            const { proof, circuitInputs, publicInputs } = proofs[i];
            const inputBatchHash = batchHashes[currentMessageBatchIndex - 1];
            if (BigInt(circuitInputs.inputBatchHash).toString() !== inputBatchHash.toString()) {
                throw new Error("input batch hash mismatch.");
            }
            const outputBatchHash = batchHashes[currentMessageBatchIndex];
            if (BigInt(circuitInputs.outputBatchHash).toString() !== outputBatchHash.toString()) {
                throw new Error("output batch hash mismatch.");
            }
            let currentSbCommitmentOnChain;
            if (numberBatchesProcessed === 0) {
                currentSbCommitmentOnChain = BigInt(await this.pollContract.currentSbCommitment());
            }
            else {
                currentSbCommitmentOnChain = BigInt(await this.messageProcessorContract.sbCommitment());
            }
            this.validateCommitment(circuitInputs.currentSbCommitment, currentSbCommitmentOnChain);
            if (circuitInputs.coordinatorPublicKeyHash.toString() !== coordinatorPublicKeyHash.toString()) {
                throw new Error("Coordinator public key mismatch.");
            }
            const formattedProof = (0, utils_1.formatProofForVerifierContract)(proof);
            const publicInputsOnChain = await this.messageProcessorContract
                .getPublicCircuitInputs(currentMessageBatchIndex, (0, utils_1.asHex)(circuitInputs.newSbCommitment), outputBatchHash.toString())
                .then((value) => [...value]);
            this.validatePublicInput(publicInputs, publicInputsOnChain);
            const verifyingKey = new domainobjs_1.VerifyingKey(new crypto_1.G1Point(onChainProcessVerifyingKey.alpha1[0], onChainProcessVerifyingKey.alpha1[1]), new crypto_1.G2Point(onChainProcessVerifyingKey.beta2[0], onChainProcessVerifyingKey.beta2[1]), new crypto_1.G2Point(onChainProcessVerifyingKey.gamma2[0], onChainProcessVerifyingKey.gamma2[1]), new crypto_1.G2Point(onChainProcessVerifyingKey.delta2[0], onChainProcessVerifyingKey.delta2[1]), onChainProcessVerifyingKey.ic.map(([x, y]) => new crypto_1.G1Point(x, y)));
            // verify the proof on chain using the verifier contract
            const isValidOnChain = await this.verifierContract.verify(formattedProof, verifyingKey.asContractParam(), [...publicInputsOnChain]);
            if (!isValidOnChain) {
                throw new Error("The verifier contract found the proof invalid.");
            }
            try {
                // validate process messaging proof and store the new state and ballot root commitment
                const receipt = await this.messageProcessorContract
                    .processMessages((0, utils_1.asHex)(circuitInputs.newSbCommitment), formattedProof)
                    .then((tx) => tx.wait());
                if (receipt?.status !== 1) {
                    throw new Error("processMessages() failed.");
                }
                (0, logger_1.logGreen)({ text: (0, logger_1.success)(`Process messages transaction hash: ${receipt.hash}`) });
                // Wait for the node to catch up
                numberBatchesProcessed = await this.messageProcessorContract.totalBatchesProcessed().then(Number);
                (0, logger_1.logMagenta)({ text: (0, logger_1.info)(`Progress: ${numberBatchesProcessed} / ${totalMessageBatches}`) });
            }
            catch (err) {
                throw new Error(`processMessages() failed: ${err.message}`);
            }
        }
        if (numberBatchesProcessed === totalMessageBatches) {
            (0, logger_1.logGreen)({ text: (0, logger_1.success)("All message processing proofs have been submitted.") });
        }
    }
    /**
     * Prove tally on-chain
     *
     * @param proofs tally proofs
     */
    async proveTally(proofs) {
        const [treeDepths, totalSignupsAndMessages, tallyBatchNumber, mode, stateTreeDepth] = await Promise.all([
            this.pollContract.treeDepths(),
            this.pollContract.totalSignupsAndMessages(),
            this.tallyContract.tallyBatchNum().then(Number),
            this.tallyContract.mode(),
            this.maciContract.stateTreeDepth().then(Number),
        ]);
        const onChainTallyVerifyingKey = await this.verifyingKeysRegistryContract.getTallyVerifyingKey(stateTreeDepth, treeDepths.tallyProcessingStateTreeDepth, treeDepths.voteOptionTreeDepth, mode);
        const totalSignups = Number(totalSignupsAndMessages[0]);
        const tallyBatchSize = core_1.STATE_TREE_ARITY ** Number(treeDepths.tallyProcessingStateTreeDepth);
        // vote tallying proofs
        const totalTallyBatches = totalSignups % tallyBatchSize === 0
            ? Math.floor(totalSignups / tallyBatchSize)
            : Math.floor(totalSignups / tallyBatchSize) + 1;
        let tallyBatchNum = tallyBatchNumber;
        if (tallyBatchNum < totalTallyBatches) {
            (0, logger_1.logMagenta)({ text: (0, logger_1.info)("Submitting proofs of vote tallying...") });
        }
        for (let i = tallyBatchNum; i < totalTallyBatches; i += 1) {
            if (i === 0) {
                await this.tallyContract.updateSbCommitment().then((tx) => tx.wait());
            }
            const batchStartIndex = i * tallyBatchSize;
            const { proof, circuitInputs, publicInputs } = proofs[i];
            const currentTallyCommitmentOnChain = await this.tallyContract.tallyCommitment();
            this.validateCommitment(circuitInputs.currentTallyCommitment, currentTallyCommitmentOnChain);
            const currentSbCommitmentOnChain = await this.messageProcessorContract.sbCommitment();
            this.validateCommitment(circuitInputs.sbCommitment, currentSbCommitmentOnChain);
            const publicInputsOnChain = await this.tallyContract.getPublicCircuitInputs(batchStartIndex, (0, utils_1.asHex)(circuitInputs.newTallyCommitment));
            this.validatePublicInput(publicInputs, publicInputsOnChain);
            // format the tally proof so it can be verified on chain
            const formattedProof = (0, utils_1.formatProofForVerifierContract)(proof);
            const verifyingKey = new domainobjs_1.VerifyingKey(new crypto_1.G1Point(onChainTallyVerifyingKey.alpha1[0], onChainTallyVerifyingKey.alpha1[1]), new crypto_1.G2Point(onChainTallyVerifyingKey.beta2[0], onChainTallyVerifyingKey.beta2[1]), new crypto_1.G2Point(onChainTallyVerifyingKey.gamma2[0], onChainTallyVerifyingKey.gamma2[1]), new crypto_1.G2Point(onChainTallyVerifyingKey.delta2[0], onChainTallyVerifyingKey.delta2[1]), onChainTallyVerifyingKey.ic.map(([x, y]) => new crypto_1.G1Point(x, y)));
            const isValidOnChain = await this.verifierContract.verify(formattedProof, verifyingKey.asContractParam(), [...publicInputsOnChain]);
            if (!isValidOnChain) {
                throw new Error("The verifier contract found the proof invalid.");
            }
            // verify the proof on chain
            const receipt = await this.tallyContract
                .tallyVotes((0, utils_1.asHex)(circuitInputs.newTallyCommitment), formattedProof)
                .then((tx) => tx.wait());
            if (receipt?.status !== 1) {
                throw new Error("tallyVotes() failed");
            }
            (0, logger_1.logMagenta)({ text: (0, logger_1.info)(`Progress: ${tallyBatchNum + 1} / ${totalTallyBatches}`) });
            (0, logger_1.logGreen)({ text: (0, logger_1.success)(`Tally votes transaction hash: ${receipt.hash}`) });
            tallyBatchNum = Number(await this.tallyContract.tallyBatchNum());
        }
        if (tallyBatchNum === totalTallyBatches) {
            (0, logger_1.logGreen)({ text: (0, logger_1.success)("All vote tallying proofs have been submitted.") });
        }
    }
    /**
     * Submit tally results on chain
     *
     * @param tallyData - tally data
     * @param recipients - number of recipients
     */
    async submitResults(tallyData, { start, end }) {
        (0, logger_1.logMagenta)({ text: (0, logger_1.info)("Submitting results...") });
        const tallyResults = tallyData.results.tally.map((t) => BigInt(t));
        // slice in case we are submitting partial results
        const partialResults = tallyResults.slice(start ?? 0, end ?? tallyResults.length);
        const [treeDepths] = await Promise.all([this.pollContract.treeDepths()]);
        const tallyResultProofs = partialResults.map((_, index) => (0, crypto_1.generateTreeProof)(index, tallyResults, Number(treeDepths.voteOptionTreeDepth)));
        await this.tallyContract
            .addTallyResults({
            voteOptionIndices: partialResults.map((_, index) => index),
            tallyResults: partialResults,
            tallyResultProofs,
            totalSpent: tallyData.totalSpentVoiceCredits.spent,
            totalSpentSalt: tallyData.totalSpentVoiceCredits.salt,
            tallyResultSalt: tallyData.results.salt,
            newResultsCommitment: tallyData.results.commitment,
            spentVoiceCreditsHash: tallyData.totalSpentVoiceCredits.commitment,
            perVOSpentVoiceCreditsHash: tallyData.perVoteOptionSpentVoiceCredits?.commitment ?? 0n,
        })
            .then((tx) => tx.wait());
        (0, logger_1.logGreen)({ text: (0, logger_1.success)("Results have been submitted.") });
    }
    /**
     * Validate commitment
     *
     * @param commitment - off-chain commitment
     * @param commitmentOnChain - on-chain commitment
     * @throws error if commitments don't match
     */
    validateCommitment(value, onchainValue) {
        if (onchainValue.toString() !== value.toString()) {
            throw new Error("commitment mismatch");
        }
    }
    /**
     * Validate public input hash
     *
     * @param publicInputs - off-chain public input hash
     * @param publicInputsOnChain - on-chain public input hash
     * @throws error if public input hashes don't match
     */
    validatePublicInput(publicInputs, publicInputsOnChain) {
        if (!publicInputsOnChain.every((value, index) => value.toString() === publicInputs[index].toString())) {
            throw new Error("public input mismatch.");
        }
    }
}
exports.Prover = Prover;
//# sourceMappingURL=Prover.js.map