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, randomHex } from 'web3-utils';
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 PrivateRangeProof66562 extends Proof {
/**
Constructs a private range proof - proving that the value of one AZTEC note, the originalNote, is greater than
the value of a second AZTEC note, the comparisonNote. The balancing relationship satisfied is:
originalNoteValue = comparisonNoteValue + utilityNoteValue
@param {Note} originalNote note whose value is being compared against the comparisonNote
@param {Note} comparisonNote note being compared against
@param {Note} utilityNote helper note used to construct a balancing relationship in the proof. The value of this note must
be chosen to satisfy the equation: originalNoteValue = comparisonNoteValue + utilityNoteValue
@param {string} sender Ethereum address of the transaction sender
@param {boolean} safeguard Boolean flag to turn on a balancing check prior to construction of proof
*/
constructor(originalNote, comparisonNote, utilityNote, sender, safeguard = true) {
const publicValue = constants.ZERO_BN;
const publicOwner = constants.addresses.ZERO_ADDRESS;
super(ProofType.PRIVATE_RANGE.name, [originalNote, comparisonNote], [utilityNote], sender, publicValue, publicOwner, [
utilityNote,
]);
if (safeguard) {
this.checkBalancingRelationShipSatisfied();
}
this.constructBlindingFactors();
this.constructChallenge();
this.constructData();
this.constructOutputs();
}
/**
* Check that notes have been supplied which satisfy the privateRange balancing relationship
*
* Balancing relationship: originalNoteValue = comparisonNoteValue + utilityNoteValue
*/
checkBalancingRelationShipSatisfied() {
const originalNoteValue = this.notes[0].k.toNumber();
const comparisonNoteValue = this.notes[1].k.toNumber();
const utilityNoteValue = this.notes[2].k.toNumber();
if (originalNoteValue !== comparisonNoteValue + utilityNoteValue) {
throw new AztecError(errors.codes.BALANCING_RELATION_NOT_SATISFIED, {
message: 'The supplied note values do not satisfy the privateRange balancing relationship',
originalNoteValue,
comparisonNoteValue,
utilityNoteValue,
});
}
}
/**
* Generate blinding factors based on the previous blinding scalars
*/
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) => {
let { bk } = blindingScalars[i];
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));
}
if (i === 2) {
const x = reducer.redPow(new BN(i + 1));
bk = blindingScalars[0].bk.redSub(blindingScalars[1].bk);
const xbk = bk.redMul(x);
const xba = ba.redMul(x);
B = note.gamma.mul(xbk).add(bn128.h.mul(xba));
}
return {
B,
bk,
ba,
};
});
}
constructChallenge() {
this.constructChallengeRecurse([this.sender, 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];
let kBar;
if (i < this.notes.length - 1) {
kBar = note.k
.redMul(this.challenge)
.redAdd(bk)
.fromRed();
} else {
kBar = new BN(randomHex(32), 16).umod(bn128.curve.n).toString(16);
}
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.PRIVATE_RANGE_PROOF, this.sender]),
);
}
/**
* Encode the privateRange proof as data for an Ethereum transaction
* @returns {Object} proof data and expected output
*/
encodeABI() {
const encodedParams = [
inputCoder.encodeProofData(this.data),
inputCoder.encodeOwners(this.inputNoteOwners),
inputCoder.encodeOwners(this.outputNoteOwners),
inputCoder.encodeMetaData(this.outputNotes),
];
// First hardcoded value in length calc is num of
// elements prepending ...offsets in abiEncodedParams
const length = 1 + encodedParams.length + 1;
const offsets = ProofUtils.getOffsets(length, encodedParams);
const abiEncodedParams = [this.challengeHex.slice(2), ...offsets, ...encodedParams];
return `0x${abiEncodedParams.join('').toLowerCase()}`;
}
}
export default PrivateRangeProof66562;