src/note/index.js

import * as bn128 from '@aztec/bn128';
import { generateAccessMetaData } from '@aztec/note-access';
import secp256k1 from '@aztec/secp256k1';
import BN from 'bn.js';
import { padLeft, toHex } from 'web3-utils';
import { noteCoder } from '../encoder';
import setup from '../setup';
import noteUtils from './utils';

const {
    constants: { ZERO_VALUE_NOTE_VIEWING_KEY },
    createSharedSecret,
    getSharedSecret,
    getNoteHash,
} = noteUtils;

/**
 * @class
 * @classdesc Class for AZTEC zero-knowledge notes. Notes have public keys and viewing keys.
 *   The viewing key is required to use note in an AZTEC zero-knowledge proof
 */
export class Note {
    /**
     * Initializes a new instance of Note from either a public key or a viewing key.
     *
     * @param {string} publicKey hex-formatted public key
     * @param {string} viewingKey hex-formatted viewing key
     * @param {Array} access mapping between an Ethereum address and the linked publickey
     * @param {string} owner Ethereum address of note owner
     * @param {Object} setupPoint trusted setup point
     */
    constructor(publicKey, viewingKey, access, owner = '0x', setupPoint) {
        if (publicKey && viewingKey) {
            throw new Error('expected one of publicKey or viewingKey, not both');
        }

        /**
         * Ethereum address of note's owner
         * @member {string}
         */
        this.owner = owner;

        /**
         * Access object, specifying Ethereum address to be granted note access
         */
        if (publicKey) {
            if (typeof publicKey !== 'string') {
                throw new Error(`expected key type ${typeof publicKey} to be of type string`);
            }
            if (publicKey.length !== 200) {
                throw new Error(`invalid public key length, expected 200, got ${publicKey.length}`);
            }
            /**
             * Viewing key of note. BN instance in bn128 group's reduction context
             * @member {BN}
             */
            this.a = null;
            /**
             * Value of note. BN instance in bn128 group's reduction context
             * @member {BN}
             */
            this.k = null;
            /**
             * AZTEC commitment point \gamma, a bn128 group element, encrypts the note value
             * @member {Point}
             */
            this.gamma = bn128.curve.decodePoint(publicKey.slice(2, 68), 'hex');
            /**
             * AZTEC commitment point \sigma, a bn128 group element, encrypts the note value
             * @member {Point}
             */
            this.sigma = bn128.curve.decodePoint(publicKey.slice(68, 134), 'hex');
            /**
             * Note's metadata - general purpose property in which useful information can be stored.
             * By default it contains the note's ephemeral key - a secp256k1 group element which
             * the note owner can use to compute the note's viewing key.
             *
             * Arbitrary additional information can be later supplied, by calling setMetaData()
             */
            this.metaData = secp256k1.compress(secp256k1.ec.keyFromPublic(publicKey.slice(134, 200), 'hex').getPublic());
        }
        if (viewingKey) {
            if (typeof viewingKey !== 'string') {
                throw new Error(`expected key type ${typeof viewingKey} to be of type string`);
            }
            if (viewingKey.length !== 140) {
                throw new Error(`invalid viewing key length, expected 140, got ${viewingKey.length}`);
            }
            this.a = new BN(viewingKey.slice(2, 66), 16).toRed(bn128.groupReduction);
            this.k = new BN(viewingKey.slice(66, 74), 16).toRed(bn128.groupReduction);
            const { x, y } = setupPoint;
            const mu = bn128.curve.point(x, y);
            this.gamma = mu.mul(this.a);
            this.sigma = this.gamma.mul(this.k).add(bn128.h.mul(this.a));
            this.metaData = secp256k1.compress(secp256k1.ec.keyFromPublic(viewingKey.slice(74, 140), 'hex').getPublic());
        }
        /**
         * keccak256 hash of note coordinates, aligned in 32-byte chunks.
         *  Alignment is [gamma.x, gamma.y, sigma.x, sigma.y]
         * @member {string}
         */
        this.noteHash = getNoteHash(this.gamma, this.sigma);

        // Grant view access to the addresses specified in access
        if (access) {
            this.grantViewAccess(access);
        }
    }

    /**
     * Compute value of a note, from the public key and the spending key
     *
     * @param {string} spendingKey the key that allows the holder to spend the note
     * @returns {string} hex-string concatenation of the note coordinates and the ephemeral key (compressed)
     */
    async derive(spendingKey) {
        const sharedSecret = getSharedSecret(this.ephemeralFromMetaData().getPublic(), spendingKey);
        this.a = new BN(sharedSecret.slice(2), 16).toRed(bn128.groupReduction);
        const gammaK = this.sigma.add(bn128.h.mul(this.a).neg());
        this.k = new BN(await bn128.recoverMessage(this.gamma, gammaK)).toRed(bn128.groupReduction);
    }

    /**
     * Export note's ephemeral key in compressed string form
     *
     * @returns {string} hex-string compressed ephemeral key
     */
    exportEphemeralKey() {
        return `0x${this.ephemeralFromMetaData().getPublic(true, 'hex')}`;
    }

    /**
     * Extract the ephemeralKey from the metaData and return it in secp256k1.ec key form
     */
    ephemeralFromMetaData() {
        return secp256k1.ec.keyFromPublic(secp256k1.decompressHex(this.metaData.slice(2, 68)), 'hex');
    }

    /**
     * Export the metaData of the note
     */
    exportMetaData() {
        const metaDataLength = this.metaData.slice(2).length / 2;
        return `${padLeft(metaDataLength, 64)}${this.metaData.slice(2)}`.slice(2);
    }

    /**
     * Export note coordinates in a form that can be used by proofs
     *
     * @returns {{ publicKey:string, viewingKey: string, k: string, a: string, noteHash: string }}
     */
    exportNote() {
        const publicKey = this.getPublic();
        const viewingKey = this.getView();
        let k = '0x';
        let a = '0x';
        if (BN.isBN(this.k)) {
            k = padLeft(this.k.fromRed().toString(16), 64);
        }
        if (BN.isBN(this.a)) {
            a = padLeft(this.a.fromRed().toString(16), 64);
        }
        return {
            publicKey,
            viewingKey,
            k,
            a,
            noteHash: this.noteHash,
        };
    }

    /**
     * Get the public key representation of a note
     *
     * @returns {string} hex-string concatenation of the note coordinates and the ephemeral key (compressed)
     */
    getPublic() {
        const ephemeral = this.ephemeralFromMetaData().getPublic();
        return noteCoder.encodeNotePublicKey({ gamma: this.gamma, sigma: this.sigma, ephemeral });
    }

    /**
     * Get the viewing key of a note
     *
     * @returns {string} hex-string concatenation of the note value and AZTEC viewing key
     */
    getView() {
        if (!BN.isBN(this.k) || !BN.isBN(this.a)) {
            return '0x';
        }
        const a = padLeft(this.a.fromRed().toString(16), 64);
        const k = padLeft(this.k.fromRed().toString(16), 8);
        const ephemeral = padLeft(this.ephemeralFromMetaData().getPublic(true, 'hex'), 66);
        return `0x${a}${k}${ephemeral}`;
    }

    /**
     * Grant an Ethereum address access to the viewing key of a note
     *
     * @param {Array} access mapping between an Ethereum address and the linked publickey
     * @returns {string} customData - customMetaData which will grant the specified Ethereum address(s)
     * access to a note
     */
    grantViewAccess(access) {
        const noteViewKey = this.getView();
        const metaData = generateAccessMetaData(access, noteViewKey);
        this.setMetaData(metaData);
    }

    /**
     * Appends custom metadata onto the metaData property of the note - i.e.e appends it onto end of the ephemeral key.
     * Also encodes it according to the schema for one note
     *
     * @param {String} customData
     * @returns {String} this.metaData - note metadata with the custom data appended
     *
     */
    setMetaData(customData) {
        this.metaData = this.metaData + padLeft(customData, 64).slice(2);
        return this.metaData;
    }
}

export const utils = noteUtils;

/**
 * Create a Note instance from a recipient public key and a desired value
 *
 * @method create
 * @param {string} publicKey hex-string formatted recipient public key
 * @param {number} value value of the note
 * @param {string} noteOwner owner of the note if different from the public key
 * @param {Array} access mapping between Ethereum addresses being granted view access of a note
 * and a linkedPublicKey
 * @returns {Promise} promise that resolves to created note instance
 */

export async function create(publicKey, value, access, noteOwner) {
    const sharedSecret = createSharedSecret(publicKey);
    const a = padLeft(new BN(sharedSecret.encoded.slice(2), 16).umod(bn128.curve.n).toString(16), 64);
    const k = padLeft(toHex(value).slice(2), 8);
    const ephemeral = padLeft(sharedSecret.ephemeralKey.slice(2), 66);
    const viewingKey = `0x${a}${k}${ephemeral}`;
    const owner = noteOwner || secp256k1.ecdsa.accountFromPublicKey(publicKey);
    const setupPoint = await setup.fetchPoint(value);
    return new Note(null, viewingKey, access, owner, setupPoint);
}

/**
 * Create Note instance from a viewing key
 *
 * @method fromViewKey
 * @param {string} viewingKey the viewing key for the note
 * @returns {Promise} promise that resolves to created note instance
 */
export async function fromViewKey(viewingKey) {
    const k = new BN(viewingKey.slice(66, 74), 16).toRed(bn128.groupReduction);
    const setupPoint = await setup.fetchPoint(k.toNumber());
    const newNote = new Note(null, viewingKey, null, undefined, setupPoint);
    return newNote;
}

/**
 * Create a zero value note with from a constant `a` to make the hash of the initial totalMinted note in
 * mintable assets a constant
 *
 * @method createZeroValueNote
 * @returns {Promise} promise that resolves to created note instance
 */
export const createZeroValueNote = () => fromViewKey(ZERO_VALUE_NOTE_VIEWING_KEY);

/**
 * Create Note instance from a public key and a spending key
 *
 * @dev This doesn't work in the web version of aztec.js
 *
 * @method derive
 * @param {string} publicKey hex-string formatted note public key
 * @param {string} spendingKey hex-string formatted spending key (can also be an Ethereum private key)
 * @returns {Promise} promise that resolves to created note instance
 */
export async function derive(publicKey, spendingKey) {
    const newNote = new Note(publicKey);
    await newNote.derive(spendingKey);
    return newNote;
}

/**
 * Encode compressed metadata of an array of notes as a hex-string, with each entry occupying 33 bytes
 *
 * @method encodeMetadata
 * @param {Array} noteArray Array of notes to encode in metadata
 * @returns {string} Metadata
 */
export function encodeMetadata(noteArray) {
    return noteArray.reduce((acc, aztecNote) => {
        const ephemeral = aztecNote.exportEphemeralKey();
        return `${acc}${padLeft(ephemeral.slice(2), 66)}`; // remove elliptic.js encoding byte, broadcast metadata is always compressed
    }, '0x');
}

/**
 * Create Note instance from an event log and a spending key
 *
 * @method fromEventLog
 * @param {string} logNoteData the note data returned from an event log
 * @returns {Note} created note instance
 */
export async function fromEventLog(logNoteData, spendingKey = null) {
    const notePublicKey = noteCoder.decodeNoteFromEventLog(logNoteData);
    const newNote = new Note(notePublicKey, null);
    if (spendingKey) {
        await newNote.derive(spendingKey);
        const { address } = secp256k1.accountFromPrivateKey(spendingKey);
        newNote.owner = address;
    }
    return newNote;
}

/**
 * Create Note instance from a Note public key
 *
 * @method fromPublicKey
 * @param {string} publicKey the public key for the note
 * @returns {Note} created note instance
 */
export function fromPublicKey(publicKey) {
    return new Note(publicKey, null);
}