src/proof/base/epoch0/proof.js

import * as bn128 from '@aztec/bn128';
import { constants, errors } from '@aztec/dev-utils';
import BN from 'bn.js';
import { padLeft } from 'web3-utils';
import Keccak from '../../../keccak';
import ProofType from '../types';
import ProofUtils from './utils';

const { AztecError } = errors;

/**
 * @class
 * @class Class to create, store and pass around AZTEC proofs and related transcripts, outputs and hashes.
 */
class Proof {
    /**
     * @param {string} type one of the ProofType enum values
     * @param {Note[]} inputNotes input AZTEC notes
     * @param {Note[]} outputNotes output AZTEC notes
     * @param {string} sender Ethereum address of transaction sender
     * @param {string} publicValue public commitment being added to proof
     * @param {string} publicOwner holder of a public token being converted
     */
    constructor(type, inputNotes, outputNotes, sender, publicValue, publicOwner) {
        this.type = type;
        this.inputNotes = inputNotes;
        this.m = inputNotes.length;
        this.outputNotes = outputNotes;
        this.notes = [...inputNotes, ...outputNotes];
        this.sender = sender;
        this.publicOwner = publicOwner;
        if (BN.isBN(publicValue)) {
            this.publicValue = publicValue;
        } else if (publicValue < 0) {
            this.publicValue = bn128.groupModulus.sub(new BN(-publicValue));
        } else {
            this.publicValue = new BN(publicValue);
        }
        this.validateInputs();
        this.constructChallengeHashes();
    }

    get challengeHex() {
        return `0x${padLeft(this.challenge.toString(16), 64)}`;
    }

    get eth() {
        return {
            // Ethereum will automatically prepend a bytes array with an evm word that
            // represents the length of the bytes array
            output: `0x${this.output.slice(0x40)}`,
            outputs: `0x${this.outputs.slice(0x42)}`,
        };
    }

    get inputNoteOwners() {
        if (!this.inputNotes || this.inputNotes.length === 0) {
            return [];
        }
        return this.inputNotes.map((n) => n.owner);
    }

    get outputNoteOwners() {
        if (!this.outputNotes || this.outputNotes.length === 0) {
            return [];
        }
        return this.outputNotes.map((n) => n.owner);
    }

    constructChallengeHashes() {
        this.challengeHash = new Keccak();
        this.rollingHash = new Keccak();
        this.notes.forEach((note) => {
            this.rollingHash.appendPoint(note.gamma);
            this.rollingHash.appendPoint(note.sigma);
        });
    }

    /**
     * Compute the Fiat-Shamir heuristic-ified challenge variable.
     *   Separated out into a distinct method so that we can stub this for extractor tests
     */
    constructChallengeRecurse(inputs) {
        inputs.forEach((challengeVar) => {
            if (typeof challengeVar === 'string') {
                this.challengeHash.appendBN(new BN(challengeVar.slice(2), 16));
            } else if (typeof challengeVar === 'number') {
                this.challengeHash.appendBN(new BN(challengeVar));
            } else if (BN.isBN(challengeVar)) {
                this.challengeHash.appendBN(challengeVar.umod(bn128.curve.n));
            } else if (Array.isArray(challengeVar)) {
                this.constructChallengeRecurse(challengeVar);
            } else if (challengeVar.gamma) {
                this.challengeHash.appendPoint(challengeVar.gamma);
                this.challengeHash.appendPoint(challengeVar.sigma);
            } else if (challengeVar.B) {
                this.challengeHash.appendPoint(challengeVar.B);
            } else {
                throw new AztecError(errors.codes.NO_ADD_CHALLENGEVAR, {
                    message: 'Can not add the challenge variable to the hash',
                    challengeVar,
                    type: typeof challengeVar,
                });
            }
        });
    }

    // eslint-disable-next-line class-methods-use-this
    constructData() {}

    // eslint-disable-next-line class-methods-use-this
    constructOutput() {}

    /**
     * Validate that the inputs in the constructor are well-formed
     */
    validateInputs() {
        if (!this.type || !ProofType[this.type]) {
            throw new Error(`Proof type should be one of ${ProofType.enumValues}`);
        }
        if (!ProofUtils.isEthereumAddress(this.sender)) {
            throw new Error('sender is not an Ethereum address');
        }

        if (!this.publicValue.lt(bn128.curve.n)) {
            throw new AztecError(errors.codes.KPUBLIC_MALFORMED, {
                message: 'publicValue is too big',
                publicValue: this.publicValue,
                maxValue: bn128.curve.n,
            });
        }

        if (!ProofUtils.isEthereumAddress(this.publicOwner)) {
            throw new Error('publicOwner is not an Ethereum address');
        }

        if (this.notes.length === 0) {
            throw new Error('the input and output notes arrays cannot be both empty');
        }

        this.notes.forEach((testNote) => {
            if (!testNote.a.fromRed().lt(bn128.curve.n) || testNote.a.fromRed().eq(constants.ZERO_BN)) {
                throw new AztecError(errors.codes.VIEWING_KEY_MALFORMED, {
                    message: 'Viewing key is malformed',
                    viewingKey: testNote.a.fromRed(),
                    criteria: `Viewing key should be less than ${bn128.curve.n} and greater than zero`,
                });
            }

            if (!testNote.k.fromRed().lt(new BN(constants.K_MAX))) {
                throw new AztecError(errors.codes.NOTE_VALUE_TOO_BIG, {
                    message: 'Note value is equal to or greater than K_MAX',
                    noteValue: testNote.k.fromRed(),
                    K_MAX: constants.K_MAX,
                });
            }

            if (testNote.gamma.isInfinity() || testNote.sigma.isInfinity()) {
                throw new AztecError(errors.codes.POINT_AT_INFINITY, {
                    message: 'One of the note points is at infinity',
                    gamma: testNote.gamma.isInfinity(),
                    sigma: testNote.sigma.isInfinity(),
                });
            }

            if (!ProofUtils.validatePointOnCurve(testNote.gamma) || !ProofUtils.validatePointOnCurve(testNote.sigma)) {
                throw new AztecError(errors.codes.NOT_ON_CURVE, {
                    message: 'A note group element is not on the curve',
                    gammaOnCurve: ProofUtils.validatePointOnCurve(testNote.gamma),
                    sigmaOnCurve: ProofUtils.validatePointOnCurve(testNote.sigma),
                });
            }
        });
    }
}

export default Proof;