mina-attestations
Version:
Private Attestations on Mina
392 lines (345 loc) • 11.3 kB
text/typescript
/**
* This file contains some helpers to wrap zkpass responses in ecdsa credentials.
*
* See `ecdsa-credential.test.ts`
*/
import { DynamicBytes, DynamicSHA3 } from '../dynamic.ts';
import { assert, ByteUtils, fill, zip } from '../util.ts';
import {
EcdsaEthereum,
getHashHelper,
parseSignature,
verifyEthereumSignature,
verifyEthereumSignatureSimple,
} from './ecdsa-credential.ts';
import { Credential } from '../credential-index.ts';
import { PublicKey, Bool, Gadgets, Unconstrained, Bytes, Field } from 'o1js';
export { ZkPass, type ZkPassResponseItem };
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:
| ReturnType<typeof createCredentialZkPassPartial>
| undefined;
let fullCredential: ReturnType<typeof createCredentialZkPassFull> | undefined;
type Type = 'bytes32' | 'address';
type PublicField = string | ({ [key: string]: PublicField } & { str?: string });
type ZkPassResponseItem = {
taskId: string;
publicFields: PublicField[];
allocatorAddress: string;
publicFieldsHash: string;
allocatorSignature: string;
uHash: string;
validatorAddress: string;
validatorSignature: string;
};
async function importCredentialPartial(
owner: PublicKey,
schema: string,
response: ZkPassResponseItem,
log: (msg: string) => void = () => {},
{ 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;
}
type Field3 = [Field, Field, Field];
/**
* 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: {
schema: Bytes;
taskId: Bytes;
validatorAddress: Bytes;
allocatorAddress: Bytes;
allocatorSignature: { r: Field3; s: Field3 };
allocatorParityBit: Bool;
}) {
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: PublicKey,
schema: string,
response: ZkPassResponseItem,
log: (msg: string) => void = () => {}
) {
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: Type[], values: Uint8Array[]) {
let arrays = zip(types, values).map(([type, value]) =>
encodeParameter(type, value)
);
return ByteUtils.concat(...arrays);
}
function encodeParameter(type: Type, value: Uint8Array) {
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: PublicField[]) {
let publicData = publicFields.map((item) => {
if (typeof item === 'object') delete item.str;
return item;
});
let values: string[] = [];
function recurse(obj: PublicField) {
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);
}