src/proof/proofs/UTILITY/epoch0/publicRange/index.js

import * as bn128 from '@aztec/bn128';
import { constants, errors, proofs } from '@aztec/dev-utils';
import BN from 'bn.js';
import AbiCoder from 'web3-eth-abi';
import { keccak256, padLeft } from 'web3-utils';
import helpers from './helpers';
import { inputCoder, outputCoder } from '../../../../../encoder';
import Proof from '../../../../base/epoch0/proof';
import ProofType from '../../../../base/types';
import ProofUtils from '../../../../base/epoch0/utils';

const { AztecError } = errors;

class PublicRangeProof66563 extends Proof {
    /**
     * Constructs a public range proof that a note is greater than or equal to, or less than or
     * equal to a public integer. Control of whether a > or < proof is constructed is determined
     * by an input boolean 'isGreaterOrEqual'.
     *
     * The balancing relation being satisfied is:
     * originalNoteValue = publicComparison + utilityNoteValue
     *
     * @param {Object} originalNote the note that a user is comparing against the publicComparison
     * @param {Number} publicComparison publicly visible integer, which the note is being compared against
     * @param {string} sender Ethereum address of the transaction sender
     * @param {bool} isGreaterOrEqual modifier controlling whether this is a greater than, or less
     * than proof. If true, it is a proof that originalNoteValue > publicComparison. If false, it is a
     * proof that originalNoteValue < publicComparison
     * @param {Note} utilityNote a helper note that is needed to satisfy a cryptographic balancing relation
     * @param {boolean} safeguard Boolean flag to turn on a balancing check prior to construction of proof
     */
    constructor(originalNote, publicComparison, sender, isGreaterOrEqual, utilityNote, safeguard = true) {
        const publicValue = constants.ZERO_BN;
        const publicOwner = constants.addresses.ZERO_ADDRESS;
        super(ProofType.PUBLIC_RANGE.name, [originalNote], [utilityNote], sender, publicValue, publicOwner, [utilityNote]);

        helpers.checkPublicComparisonWellFormed(publicComparison);
        this.publicComparison = new BN(publicComparison);
        this.utilityNote = utilityNote;
        this.originalNote = originalNote;
        this.isGreaterOrEqual = isGreaterOrEqual;

        this.proofRelationChoice();

        if (safeguard) {
            this.checkBalancingRelationShipSatisfied();
        }

        this.constructBlindingFactors();
        this.constructChallenge();
        this.constructData();
        this.constructOutputs();
    }

    /**
     * Control whether the proof is used for a greater than (originalNote > publicComparison) or
     * less than (originalNote < publicComparison) proof.
     *
     * If greater than, the input publicComparison value is unchanged.
     *
     * If less than, the publicComparison value is negated; in order to satisfy the balancing
     * relationship
     */
    proofRelationChoice() {
        if (!this.isGreaterOrEqual) {
            this.publicComparison = this.publicComparison.neg();
        }
    }

    /**
     * Check that notes have been supplied which satisfy the publicRange balancing relationship
     *
     * Balancing relationship: originalNoteValue = publicComparison + utilityNoteValue
     */
    checkBalancingRelationShipSatisfied() {
        const originalNoteValue = this.notes[0].k.toNumber();
        const utilityNoteValue = this.notes[1].k.toNumber();
        const publicComparison = this.publicComparison.toNumber();

        if (originalNoteValue !== publicComparison + utilityNoteValue) {
            throw new AztecError(errors.codes.BALANCING_RELATION_NOT_SATISFIED, {
                message: 'The supplied note values do not satisfy the publicRange balancing relationship',
                originalNoteValue,
                utilityNoteValue,
                publicComparison,
            });
        }
    }

    constructBlindingFactors() {
        const blindingScalars = Array(this.notes.length)
            .fill()
            .map(() => {
                return {
                    bk: bn128.randomGroupScalar(),
                    ba: bn128.randomGroupScalar(),
                };
            });

        let B;
        const reducer = this.rollingHash.redKeccak();
        this.blindingFactors = this.notes.map((note, i) => {
            const { bk } = blindingScalars[0]; // trivially true for i=0, and enforcing k1 = k2 for i=1
            const { ba } = blindingScalars[i];

            if (i === 0) {
                B = note.gamma.mul(bk).add(bn128.h.mul(ba));
            }

            if (i === 1) {
                const x = reducer.redPow(new BN(i + 1));
                const xbk = bk.redMul(x);
                const xba = ba.redMul(x);
                B = note.gamma.mul(xbk).add(bn128.h.mul(xba));
            }
            return { bk, ba, B };
        });
    }

    constructChallenge() {
        this.constructChallengeRecurse([
            this.sender,
            this.publicComparison,
            this.publicValue,
            this.publicOwner,
            this.notes,
            this.blindingFactors,
        ]);
        this.challenge = this.challengeHash.redKeccak();
    }

    constructData() {
        this.data = this.blindingFactors.map(({ bk, ba }, i) => {
            const note = this.notes[i];
            const kBar = note.k
                .redMul(this.challenge)
                .redAdd(bk)
                .fromRed();
            const aBar = note.a
                .redMul(this.challenge)
                .redAdd(ba)
                .fromRed();

            const items = [
                kBar,
                aBar,
                note.gamma.x.fromRed(),
                note.gamma.y.fromRed(),
                note.sigma.x.fromRed(),
                note.sigma.y.fromRed(),
            ];
            return items.map((item) => `0x${padLeft(item.toString(16), 64)}`);
        });
    }

    // TODO: normalise proof output encoding. In some places it's expected to use `encodeProofOutputs`
    // while in others `encodeProofOutput`.
    constructOutputs() {
        const proofOutput = {
            inputNotes: this.inputNotes,
            outputNotes: this.outputNotes,
            publicValue: this.publicValue,
            publicOwner: this.publicOwner,
            challenge: this.challengeHex,
        };
        this.output = outputCoder.encodeProofOutput(proofOutput);
        this.outputs = outputCoder.encodeProofOutputs([proofOutput]);
        this.hash = outputCoder.hashProofOutput(this.output);
        this.validatedProofHash = keccak256(
            AbiCoder.encodeParameters(['bytes32', 'uint24', 'address'], [this.hash, proofs.PUBLIC_RANGE_PROOF, this.sender]),
        );
    }

    encodeABI() {
        const encodedParams = [
            inputCoder.encodeProofData(this.data),
            inputCoder.encodeOwners(this.inputNoteOwners),
            inputCoder.encodeOwners(this.outputNoteOwners),
            inputCoder.encodeMetaData(this.outputNotes),
        ];

        const length = 2 + encodedParams.length + 1;
        const offsets = ProofUtils.getOffsets(length, encodedParams);

        // If publicComparison < 0, make it compatible with finite field arithmetic
        if (Number(this.publicComparison) < 0) {
            const publicIntegerCastToField = bn128.groupModulus.add(this.publicComparison);
            this.publicComparison = publicIntegerCastToField;
        }
        const abiEncodedPublicInteger = padLeft(this.publicComparison.toString(16), 64);

        const abiEncodedParams = [this.challengeHex.slice(2), abiEncodedPublicInteger, ...offsets, ...encodedParams];
        return `0x${abiEncodedParams.join('').toLowerCase()}`;
    }

    validateInputs() {
        super.validateInputs();
        if (this.notes.length !== 2) {
            throw new AztecError(errors.codes.INCORRECT_NOTE_NUMBER, {
                message: `Public range proofs must contain 2 notes`,
                numNotes: this.notes.length,
            });
        }
    }
}

export default PublicRangeProof66563;