@unirep/core
Version:
Client library for protocol related functions which are used in UniRep protocol.
215 lines (214 loc) • 9.03 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.DataSchema = void 0;
const circuits_1 = require("@unirep/circuits");
/**
* The `DataSchema` class abstracts UniRep data into a JavaScript object.
* This class can be used to encode and decode attestation data,
* and build attestations that are ready to be submitted to the UniRep smart contract.
* @example
* ```ts
* import { Attestation, DataSchema, SchemaField } from '@unirep/core'
*
* const schema: SchemaField[] = [
* {name: 'posRep', type: 'uint64', updateBy: 'sum',},
* {name: 'negRep', type: 'uint64', updateBy: 'sum',},
* {name: 'graffiti', type: 'uint205', updateBy: 'replace',},
* {name: 'postCount', type: 'uint49', updateBy: 'sum',},
* {name: 'commentCount', type: 'uint49', updateBy: 'sum',},
* {name: 'voteCount', type: 'uint49', updateBy: 'sum',},
* ]
*
* const d = new DataSchema(schema)
* ```
*/
class DataSchema {
constructor(schema, config = circuits_1.CircuitConfig.default) {
this.config = config;
this.schema = this.parseSchema(schema);
}
/**
* Verify a user-defined data schema
* @param schema The array of `SchemaField`
* @returns
* ```ts
* {
* ...schema: SchemaField, // exploded `SchemaField` fields
* dataIndex: number,
* offset: number, // bit offset in attester change
* bits: number // bits allocated
* }
* ```
*/
parseSchema(schema) {
let sumOffset = 0;
let replOffset = 0;
const MAX_SAFE_BITS = Number(this.config.MAX_SAFE_BITS);
const maxSumOffset = MAX_SAFE_BITS * this.config.SUM_FIELD_COUNT;
const maxReplOffset = MAX_SAFE_BITS *
(this.config.FIELD_COUNT - this.config.SUM_FIELD_COUNT);
return schema.map((field, idx) => {
const { name, type, updateBy, ...extraFields } = field;
const match = type.match(/^uint(\d+)$/);
if (match === null)
throw new Error(`Invalid type for field ${name}: "${type}"`);
if (Object.keys(extraFields).length !== 0) {
throw new Error(`Invalid fields included for field ${name}: [${Object.keys(extraFields)}]`);
}
const duplicateEntry = schema.find((x, i) => x.name == field.name && idx != i);
if (duplicateEntry) {
throw Error(`Schema includes a duplicate entry: "${duplicateEntry.name}"`);
}
const bits = +match[1];
if (bits < 1 || bits > MAX_SAFE_BITS)
throw new Error(`Invalid uint size for field ${name}: ${bits}`);
if (updateBy === 'sum') {
if (Math.floor(sumOffset / MAX_SAFE_BITS) !==
Math.floor((sumOffset + bits - 1) / MAX_SAFE_BITS)) {
sumOffset += MAX_SAFE_BITS - (sumOffset % MAX_SAFE_BITS);
}
if (sumOffset + bits > maxSumOffset) {
throw new Error(`Invalid schema, field "${name}" exceeds available storage`);
}
const dataIndex = Math.floor(sumOffset / MAX_SAFE_BITS);
const offset = sumOffset % MAX_SAFE_BITS;
sumOffset += bits;
return { ...field, dataIndex, offset, bits };
}
else if (updateBy === 'replace') {
if (bits !== MAX_SAFE_BITS - this.config.REPL_NONCE_BITS)
throw new Error(`Field must be ${MAX_SAFE_BITS - this.config.REPL_NONCE_BITS} bits`);
if (replOffset + bits > maxReplOffset) {
throw new Error(`Invalid schema, field "${name}" exceeds available storage`);
}
const dataIndex = this.config.SUM_FIELD_COUNT +
Math.floor(replOffset / MAX_SAFE_BITS);
const offset = replOffset % MAX_SAFE_BITS;
replOffset += bits;
return { ...field, dataIndex, offset, bits };
}
throw new Error(`Invalid updateBy strategy for field ${name}: "${updateBy}"`);
});
}
/**
* Build an `Attestation` object to be used for a UniRep contract
* @param change The data change. If it is `sum` field, the data will be changed by addition. If it is `replacement` field, the data will be changed by replacement.
* @returns The attestation object will be submitted to the Unirep contract.
* @example
* **Sum field**
* ```ts
* // 10 will be added to the 'posRep' field in the user data
* const sumChange = { name: 'posRep', val: BigInt(10) }
* const sumAttestation: Attestation = d.buildAttestation(sumChange)
* ```
*
* **Replacement field**
* ```ts
* // 20 will replace the current value in the 'graffiti' field in user data
* const replacementChange = { name: 'graffiti', val: BigInt(20) }
* const replacementAttestation: Attestation = d.buildAttestation(replacementChange)
* ```
*/
buildAttestation(change) {
const field = this.schema.find((f) => f.name === change.name);
if (field === undefined) {
throw new Error(`${change.name} not found`);
}
const fieldIndex = field.dataIndex;
const x = change.val << BigInt(field.offset);
const maxVal = (BigInt(1) << BigInt(field.bits)) - BigInt(1);
if (x > maxVal << BigInt(field.offset)) {
throw new Error(`${change.name} exceeds allocated space`);
}
const attestation = {
fieldIndex,
change: x,
};
return attestation;
}
/**
* Build multiple `Attestation` objects to be used for a UniRep contract
* @param changes The array of data change.
* @returns The array of attestations will be submitted to the Unirep contract.
* @example
* ```ts
* // Multiple attestations can be built using `buildAttestations()`
* const changes = [
* { name: 'posRep', val: BigInt(10) },
* { name: 'negRep', val: BigInt(10) },
* { name: 'negRep', val: BigInt(20) },
* { name: 'graffiti', val: BigInt(30) },
* ]
*
* //Returns two `Attestation` objects: 'posRep' and 'negRep' attestations are combined into one attestation
* const attestations: Attestation[] = d.buildAttestations(changes)
* ```
*/
buildAttestations(changes) {
const attestations = Array(this.schema.length).fill(null);
for (const change of changes) {
const field = this.schema.find((f) => f.name === change.name);
if (field === undefined) {
throw new Error(`${change.name} not found`);
}
const maxVal = (BigInt(1) << BigInt(field.bits)) - BigInt(1);
const fieldIndex = field.dataIndex;
let v = change.val << BigInt(field.offset);
// Get existing attestation sum value
let prevVal = BigInt(0);
if (field.updateBy === 'sum' && attestations[fieldIndex] !== null)
prevVal =
(attestations[fieldIndex].change >> BigInt(field.offset)) &
maxVal;
if (v + prevVal > maxVal << BigInt(field.offset)) {
throw new Error(`${change.name} exceeds allocated space`);
}
// Include previous attestation change value in our new attestation
// This is necessary to get the value of other schema fields in the attestation
if (attestations[fieldIndex] !== null && field.updateBy === 'sum')
v += attestations[fieldIndex].change;
attestations[fieldIndex] = {
fieldIndex,
change: v,
};
}
return attestations.filter((attestation) => attestation !== null);
}
/**
* Parse encoded schema, producing a dictionary of user-defined field names and attestation values
* @param data The raw data appended to the Unirep contract.
* @returns The names of the data and its values.
* @example
* ```ts
* // JS literal representing emitted data from a UniRep contract
* const data = [
* 553402322211286548490n,
* 0n,
* 0n,
* 0n,
* 205688069665150755269371147819668813122841983204197482918576158n,
* 0n
* ]
*
* const parsedData = d.parseData(data)
* // Result:
* // parsedData = {
* // posRep: 10n,
* // negRep: 30n,
* // graffiti: 30n,
* // postCount: 0n,
* // commentCount: 0n,
* // voteCount: 0n
* // }
* ```
*/
parseData(data) {
const parsed = {};
for (const field of this.schema) {
const { name, /* type, updateBy, */ dataIndex, bits, offset } = field;
parsed[name] = (0, circuits_1.shiftBits)(data[dataIndex], BigInt(offset), BigInt(bits));
}
return parsed;
}
}
exports.DataSchema = DataSchema;