Skip to content

Commit

Permalink
Merge pull request #49 from zksecurity/feature/nullifier-example
Browse files Browse the repository at this point in the history
Nullifier example and polishing
  • Loading branch information
mitschabaude authored Oct 28, 2024
2 parents 3b3954a + 0f400ae commit 8a7c3d4
Show file tree
Hide file tree
Showing 18 changed files with 612 additions and 155 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ jobs:
npm ci
npm run build --if-present
npm test
npm run examples
env:
CI: true
106 changes: 106 additions & 0 deletions examples/unique-hash.eg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Bytes } from 'o1js';
import {
Spec,
Operation,
Claim,
Credential,
Presentation,
PresentationRequest,
assert,
} from '../src/index.ts';
import { issuerKey, owner, ownerKey } from '../tests/test-utils.ts';
import { validateCredential } from '../src/credential-index.ts';
import { array } from '../src/o1js-missing.ts';

// example schema of the credential, which has enough entropy to be hashed into a unique id
const Bytes32 = Bytes(32);
const Bytes16 = Bytes(16); // 16 bytes = 128 bits = enough entropy

const Data = { nationality: Bytes32, id: Bytes16 };

// ---------------------------------------------
// ISSUER: issue a signed credential to the owner

let data = {
nationality: Bytes32.fromString('United States of America'),
id: Bytes16.random(),
};
let credential = Credential.sign(issuerKey, { owner, data });
let credentialJson = Credential.toJSON(credential);

console.log('✅ ISSUER: issued credential:', credentialJson);

// ---------------------------------------------
// WALLET: deserialize, validate and store the credential

let storedCredential = Credential.fromJSON(credentialJson);

await validateCredential(storedCredential);

console.log('✅ WALLET: imported and validated credential');

// ---------------------------------------------
// VERIFIER: request a presentation

const spec = Spec(
{
signedData: Credential.Simple(Data), // schema needed here!
targetNationalities: Claim(array(Bytes32, 3)), // TODO would make more sense as dynamic array
appId: Claim(Bytes32),
},
({ signedData, targetNationalities, appId }) => ({
// we assert that the owner has the target nationality
// TODO: add a one-of-many operation to make this more interesting
assert: Operation.equalsOneOf(
Operation.property(signedData, 'nationality'),
targetNationalities
),
// we expose a unique hash of the credential data, as nullifier
data: Operation.record({
nullifier: Operation.hash(signedData, appId),
}),
})
);

const targetNationalities = ['United States of America', 'Canada', 'Mexico'];

let request = PresentationRequest.noContext(spec, {
targetNationalities: targetNationalities.map((s) => Bytes32.fromString(s)),
appId: Bytes32.fromString('my-app-id:123'),
});
let requestJson = PresentationRequest.toJSON(request);

console.log('✅ VERIFIER: created presentation request:', requestJson);

// ---------------------------------------------
// WALLET: deserialize request and create presentation

console.time('compile');
let deserialized = PresentationRequest.fromJSON<typeof request>(requestJson);
let compiled = await Presentation.compile(deserialized);
console.timeEnd('compile');

console.time('create');
let presentation = await Presentation.create(ownerKey, {
request: compiled,
credentials: [storedCredential],
});
console.timeEnd('create');
// TODO: to send the presentation back we need to serialize it as well

console.log('✅ WALLET: created presentation:', presentation);

// ---------------------------------------------
// VERIFIER: verify the presentation, and check that the nullifier was not used yet

let existingNullifiers = new Set([0x13c43f30n, 0x370f3473n, 0xe1fe0cdan]);

// TODO: claims and other I/O values should be plain JS types
let { nullifier } = presentation.outputClaim;
assert(
!existingNullifiers.has(nullifier.toBigInt()),
'Nullifier should be unique'
);
console.log('✅ VERIFIER: checked nullifier uniqueness');

// TODO: implement verification
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"format": "prettier --write --ignore-unknown **/*",
"test": "node --test --experimental-strip-types --no-warnings {tests,src}/**/*.test.ts",
"test-one": "node --enable-source-maps --test --experimental-strip-types --no-warnings",
"examples": "node --test --experimental-strip-types --no-warnings examples/*.eg.ts",
"extension:dev": "vite build --config browser-extension/vite.config.js --watch",
"extension:build": "vite build --config browser-extension/vite.config.js"
},
Expand Down
104 changes: 100 additions & 4 deletions src/credential-index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
import { createUnsigned, Unsigned } from './credential.ts';
import { createSigned, Signed } from './credential-signed.ts';
import { type PublicKey } from 'o1js';
import { Recursive } from './credential-recursive.ts';
import {
createUnsigned,
type CredentialSpec,
type CredentialType,
hashCredential,
type StoredCredential,
Unsigned,
} from './credential.ts';
import {
createSigned,
Signed,
type Witness as SignedWitness,
} from './credential-signed.ts';
import {
Recursive,
type Witness as RecursiveWitness,
} from './credential-recursive.ts';
import { assert, hasProperty } from './util.ts';
import {
type InferNestedProvable,
NestedProvable,
type NestedProvablePure,
} from './nested.ts';
import { assertPure } from './o1js-missing.ts';
import { serializeNestedProvableValue } from './serialize-spec.ts';
import { deserializeNestedProvableValue } from './deserialize-spec.ts';

export { Credential };
export { Credential, validateCredential };

/**
* A credential is a generic piece of data (the "attributes") along with an owner represented by a public key.
Expand All @@ -24,4 +47,77 @@ const Credential = {
* Create a dummy credential with no owner and no signature.
*/
unsigned: createUnsigned,

/**
* Serialize a credential to a JSON string.
*/
toJSON(credential: StoredCredential) {
let json = {
version: credential.version,
witness: serializeNestedProvableValue(credential.witness),
metadata: credential.metadata,
credential: serializeNestedProvableValue(credential.credential),
};
return JSON.stringify(json);
},

/**
* Deserialize a credential from a JSON string.
*/
fromJSON(json: string): StoredCredential {
let obj = JSON.parse(json);
return {
version: obj.version,
witness: deserializeNestedProvableValue(obj.witness),
metadata: obj.metadata,
credential: deserializeNestedProvableValue(obj.credential),
};
},
};

// validating generic credential

type Witness = SignedWitness | RecursiveWitness;

async function validateCredential(
credential: StoredCredential<unknown, unknown, unknown>
) {
assert(
credential.version === 'v0',
`Unsupported credential version: ${credential.version}`
);

assert(knownWitness(credential.witness), 'Unknown credential type');

// TODO: this is brittle. probably data type should be part of metadata.
let data = NestedProvable.get(
NestedProvable.fromValue(credential.credential.data)
);
assertPure(data);
let spec = getCredentialSpec(credential.witness)(data);

let credHash = hashCredential(data, credential.credential);
await spec.verifyOutsideCircuit(credential.witness, credHash);
}

const witnessTypes = new Set<unknown>([
'simple',
'recursive',
] satisfies Witness['type'][]);

function knownWitness(witness: unknown): witness is Witness {
return hasProperty(witness, 'type') && witnessTypes.has(witness.type);
}

function getCredentialSpec<W extends Witness>(
witness: W
): <DataType extends NestedProvablePure>(
dataType: DataType
) => CredentialSpec<CredentialType, W, InferNestedProvable<DataType>> {
switch (witness.type) {
case 'simple':
return Credential.Simple as any;
case 'recursive':
return Credential.Recursive.Generic as any;
}
}
63 changes: 58 additions & 5 deletions src/credential-recursive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FeatureFlags,
Proof,
Poseidon,
verify,
} from 'o1js';
import {
assertPure,
Expand All @@ -21,15 +22,17 @@ import {
} from './nested.ts';
import { prefixes } from './constants.ts';
import {
type CredentialType,
type CredentialSpec,
type Credential,
type StoredCredential,
HashableCredential,
defineCredential,
} from './credential.ts';
import { assert } from './util.ts';

export { Recursive };
export { Recursive, type Witness };

type Witness<Data, Input> = {
type Witness<Data = any, Input = any> = {
type: 'recursive';
vk: VerificationKey;
proof: DynamicProof<Input, Credential<Data>>;
Expand All @@ -49,14 +52,14 @@ function Recursive<
>(
Proof: typeof DynamicProof<Input, Credential<Data>>,
dataType: DataType
): CredentialType<'proof', Witness<Data, Input>, Data> {
): CredentialSpec<'recursive', Witness<Data, Input>, Data> {
// TODO annoying that this cast doesn't work without overriding the type
let data: NestedProvablePureFor<Data> = dataType as any;
const credentialType = HashableCredential(data);

return {
type: 'credential',
id: 'proof',
credentialType: 'recursive',
witness: {
type: ProvableType.constant('recursive' as const),
vk: VerificationKey,
Expand All @@ -70,6 +73,12 @@ function Recursive<
let credential = credHash.unhash();
Provable.assertEqual(credentialType, proof.publicOutput, credential);
},
async verifyOutsideCircuit({ vk, proof }, credHash) {
let ok = await verify(proof, vk);
assert(ok, 'Invalid proof');
let credential = credHash.unhash();
Provable.assertEqual(credentialType, proof.publicOutput, credential);
},

// issuer == hash of vk and public input
issuer({ vk, proof }) {
Expand All @@ -84,7 +93,51 @@ function Recursive<
};
}

const GenericRecursive = defineCredential({
credentialType: 'recursive',
witness: {
type: ProvableType.constant('recursive' as const),
vk: VerificationKey,
proof: DynamicProof,
},

// verify the proof, check that its public output is exactly the credential
verify({ vk, proof }, credHash) {
proof.verify(vk);
let credential = credHash.unhash();
Provable.assertEqual(
(proof.constructor as typeof DynamicProof).publicOutputType,
proof.publicOutput,
credential
);
},
async verifyOutsideCircuit({ vk, proof }, credHash) {
let ok = await verify(proof, vk);
assert(ok, 'Invalid proof');
let credential = credHash.unhash();
Provable.assertEqual(
(proof.constructor as typeof DynamicProof).publicOutputType,
proof.publicOutput,
credential
);
},

// issuer == hash of vk and public input
issuer({ vk, proof }) {
let credIdent = Poseidon.hash(
(proof.constructor as typeof DynamicProof).publicInputType.toFields(
proof.publicInput
)
);
return Poseidon.hashWithPrefix(prefixes.issuerRecursive, [
vk.hash,
credIdent,
]);
},
});

Recursive.fromProgram = RecursiveFromProgram;
Recursive.Generic = GenericRecursive;

async function RecursiveFromProgram<
DataType extends ProvablePure<any>,
Expand Down
6 changes: 5 additions & 1 deletion src/credential-signed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Metadata = undefined;
type Signed<Data> = StoredCredential<Data, Witness, Metadata>;

const Signed = defineCredential({
id: 'signature-native',
credentialType: 'simple',
witness: {
type: ProvableType.constant('simple' as const),
issuer: PublicKey,
Expand All @@ -38,6 +38,10 @@ const Signed = defineCredential({
let ok = issuerSignature.verify(issuer, [credHash.hash]);
ok.assertTrue('Invalid signature');
},
async verifyOutsideCircuit({ issuer, issuerSignature }, credHash) {
let ok = issuerSignature.verify(issuer, [credHash.hash]);
ok.assertTrue('Invalid signature');
},

// issuer == issuer public key
issuer({ issuer }) {
Expand Down
Loading

0 comments on commit 8a7c3d4

Please sign in to comment.