mina-attestations
Version:
Private Attestations on Mina
241 lines • 10.6 kB
JavaScript
/**
* This file contains some helpers to wrap zkpass responses in ecdsa credentials.
*
* See `ecdsa-credential.test.ts`
*/
import { DynamicBytes, DynamicSHA3 } from "../dynamic.js";
import { assert, ByteUtils, fill, zip } from "../util.js";
import { EcdsaEthereum, getHashHelper, parseSignature, verifyEthereumSignature, verifyEthereumSignatureSimple, } from "./ecdsa-credential.js";
import { Credential } from "../credential-index.js";
import { Bool, Gadgets, Unconstrained, Bytes } from 'o1js';
export { ZkPass };
const { Signature, Address } = EcdsaEthereum;
const maxMessageLength = 128;
const Message = DynamicBytes({ maxLength: maxMessageLength });
const Bytes32 = Bytes(32);
/**
* Utilities to help process zkpass responses.
*/
const ZkPass = {
importCredentialPartial,
verifyPublicInput,
encodeParameters,
genPublicFieldHash,
CredentialPartial() {
return (partialCredential ??= createCredentialZkPassPartial());
},
async compileDependenciesPartial({ proofsEnabled = true } = {}) {
await getHashHelper(maxMessageLength).compile({ proofsEnabled });
let cred = await ZkPass.CredentialPartial();
await cred.compile({ proofsEnabled });
},
CredentialFull() {
return (fullCredential ??= createCredentialZkPassFull());
},
async compileDependenciesFull({ proofsEnabled = true } = {}) {
await getHashHelper(maxMessageLength).compile({ proofsEnabled });
let cred = await ZkPass.CredentialFull();
await cred.compile({ proofsEnabled });
},
};
let partialCredential;
let fullCredential;
async function importCredentialPartial(owner, schema, response, log = () => { }, { proofsEnabled = true } = {}) {
// validate public fields hash
let recoveredPublicFieldsHash = ZkPass.genPublicFieldHash(response.publicFields).toBytes();
assert('0x' + ByteUtils.toHex(recoveredPublicFieldsHash) ===
response.publicFieldsHash);
let schemaBytes = encodeParameter('bytes32', ByteUtils.fromString(schema));
let taskId = encodeParameter('bytes32', ByteUtils.fromString(response.taskId));
let uHash = encodeParameter('bytes32', ByteUtils.fromHex(response.uHash));
let publicFieldsHash = encodeParameter('bytes32', recoveredPublicFieldsHash);
let { signature: validatorSignature, parityBit: validatorParityBit } = parseSignature(response.validatorSignature);
let validatorAddress = ByteUtils.fromHex(response.validatorAddress);
let { signature: allocatorSignature, parityBit: allocatorParityBit } = parseSignature(response.allocatorSignature);
let allocatorAddress = ByteUtils.fromHex(response.allocatorAddress);
log('Compiling ZkPass credential...');
await ZkPass.compileDependenciesPartial({ proofsEnabled });
let ZkPassCredential = await ZkPass.CredentialPartial();
log('Creating ZkPass credential...');
let credential = await ZkPassCredential.create({
owner,
publicInput: {
schema: Bytes32.from(schemaBytes),
taskId: Bytes32.from(taskId),
allocatorAddress: Address.from(allocatorAddress),
validatorAddress: Address.from(validatorAddress),
allocatorSignature,
allocatorParityBit,
},
privateInput: {
publicFieldsHash: Bytes32.from(publicFieldsHash),
uHash: Bytes32.from(uHash),
validatorSignature,
validatorParityBit,
},
});
return credential;
}
/**
* Verify the public input of a partial zkpass credential.
*
* Returns the allocator address and schema id for further verification against
* expected values.
*
* This is a verification step that can be done in the clear, and is
* outsourced from the proof to the verifier.
*/
function verifyPublicInput(publicInput) {
let allocatorMessage = DynamicBytes.from([
...publicInput.taskId.bytes,
...publicInput.schema.bytes,
...fill(12, 0x00), // 12 zero bytes to fill up address
...publicInput.validatorAddress.bytes,
]);
verifyEthereumSignatureSimple(allocatorMessage, Signature.from(publicInput.allocatorSignature), publicInput.allocatorAddress, Unconstrained.from(publicInput.allocatorParityBit.toBoolean()));
return {
allocatorAddress: publicInput.allocatorAddress.toHex(),
schema: ByteUtils.toString(publicInput.schema.toBytes()),
};
}
function createCredentialZkPassPartial() {
return Credential.Imported.fromMethod({
name: `zkpass-partial-${maxMessageLength}`,
publicInput: {
schema: Bytes32,
taskId: Bytes32,
validatorAddress: Address,
allocatorAddress: Address,
allocatorSignature: { r: Gadgets.Field3, s: Gadgets.Field3 },
allocatorParityBit: Bool,
},
privateInput: {
uHash: Bytes32,
publicFieldsHash: Bytes32,
validatorSignature: Signature,
validatorParityBit: Unconstrained.withEmpty(false),
},
data: { nullifier: Bytes32, publicFieldsHash: Bytes32 },
}, async ({ publicInput: { schema, taskId, validatorAddress }, privateInput: { uHash, publicFieldsHash, validatorSignature, validatorParityBit, }, }) => {
// combine inputs to validator message
let validatorMessage = DynamicBytes.from([
...taskId.bytes,
...schema.bytes,
...uHash.bytes,
...publicFieldsHash.bytes,
]);
// Verify validator signature
await verifyEthereumSignature(validatorMessage, validatorSignature, validatorAddress, validatorParityBit, maxMessageLength);
return { nullifier: uHash, publicFieldsHash };
});
}
// Verifies both validator and allocator signatures
async function importCredentialFull(owner, schema, response, log = () => { }) {
let publicFieldsHash = ZkPass.genPublicFieldHash(response.publicFields).toBytes();
// validate public fields hash
assert('0x' + ByteUtils.toHex(publicFieldsHash) === response.publicFieldsHash);
// compute allocator message hash
let allocatorMessage = ZkPass.encodeParameters(['bytes32', 'bytes32', 'address'], [
ByteUtils.fromString(response.taskId),
ByteUtils.fromString(schema),
ByteUtils.fromHex(response.validatorAddress),
]);
// compute validator message hash
let validatorMessage = ZkPass.encodeParameters(['bytes32', 'bytes32', 'bytes32', 'bytes32'], [
ByteUtils.fromString(response.taskId),
ByteUtils.fromString(schema),
ByteUtils.fromHex(response.uHash),
publicFieldsHash,
]);
let { signature: allocatorSignature, parityBit: allocatorParityBit } = parseSignature(response.allocatorSignature);
let { signature: validatorSignature, parityBit: validatorParityBit } = parseSignature(response.validatorSignature);
let allocatorAddress = ByteUtils.fromHex(response.allocatorAddress);
let validatorAddress = ByteUtils.fromHex(response.validatorAddress);
log('Compiling ZkPass full credential...');
await ZkPass.compileDependenciesFull();
let ZkPassCredential = await ZkPass.CredentialFull();
log('Creating ZkPass full credential...');
let credential = await ZkPassCredential.create({
owner,
publicInput: {
allocatorAddress: EcdsaEthereum.Address.from(allocatorAddress),
},
privateInput: {
allocatorMessage,
allocatorSignature,
allocatorParityBit,
validatorMessage,
validatorSignature,
validatorParityBit,
validatorAddress: EcdsaEthereum.Address.from(validatorAddress),
},
});
return credential;
}
// Verifies both validator and allocator signatures
// TODO: OOM
function createCredentialZkPassFull() {
return Credential.Imported.fromMethod({
name: `zkpass-full-${maxMessageLength}`,
publicInput: { allocatorAddress: Address },
privateInput: {
allocatorMessage: Message,
allocatorSignature: Signature,
allocatorParityBit: Unconstrained.withEmpty(false),
validatorMessage: Message,
validatorSignature: Signature,
validatorParityBit: Unconstrained.withEmpty(false),
validatorAddress: Address,
},
data: { allocatorMessage: Message, validatorMessage: Message },
}, async ({ publicInput: { allocatorAddress }, privateInput: { allocatorMessage, allocatorSignature, allocatorParityBit, validatorMessage, validatorSignature, validatorParityBit, validatorAddress, }, }) => {
// Verify allocator signature
await verifyEthereumSignature(allocatorMessage, allocatorSignature, allocatorAddress, allocatorParityBit, maxMessageLength);
// Verify validator signature
await verifyEthereumSignature(validatorMessage, validatorSignature, validatorAddress, validatorParityBit, maxMessageLength);
return { allocatorMessage, validatorMessage };
});
}
function encodeParameters(types, values) {
let arrays = zip(types, values).map(([type, value]) => encodeParameter(type, value));
return ByteUtils.concat(...arrays);
}
function encodeParameter(type, value) {
if (type === 'bytes32')
return ByteUtils.padEnd(value, 32, 0);
if (type === 'address')
return ByteUtils.padStart(value, 32, 0);
throw Error('unexpected type');
}
// hash used by zkpass to commit to public fields
// FIXME unfortunately this does nothing to prevent collisions -.-
function genPublicFieldHash(publicFields) {
let publicData = publicFields.map((item) => {
if (typeof item === 'object')
delete item.str;
return item;
});
let values = [];
function recurse(obj) {
if (typeof obj === 'string') {
values.push(obj);
return;
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
recurse(obj[key]); // it's a nested object, so we do it again
}
else {
values.push(obj[key]); // it's not an object, so we just push the value
}
}
}
}
publicData.forEach((data) => recurse(data));
let publicFieldStr = values.join('');
if (publicFieldStr === '')
publicFieldStr = '1'; // ??? another deliberate collision
return DynamicSHA3.keccak256(publicFieldStr);
}
//# sourceMappingURL=zkpass.js.map