-
Notifications
You must be signed in to change notification settings - Fork 121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Using a side-loaded verification key throws an error in the SmartContract method when using ZkProgram chaining #1854
Comments
Maybe related to #1561 |
Chaining two ZkPrograms that use side-loading keys throws a similar error (this error does not occur if only one ZkProgram with side-loaded key is used):
Test code: import { describe, expect, it } from "@jest/globals";
import {
Mina,
PrivateKey,
DynamicProof,
VerificationKey,
Void,
ZkProgram,
Field,
SmartContract,
method,
AccountUpdate,
state,
State,
Cache,
} from "o1js";
class ProgramProof extends DynamicProof<Field, Void> {
static publicInputType = Field;
static publicOutputType = Void;
static maxProofsVerified = 0 as const;
}
const program1 = ZkProgram({
name: "program1",
publicInput: Field,
methods: {
check: {
privateInputs: [Field],
async method(publicInput: Field, field: Field) {
publicInput.assertEquals(field);
},
},
},
});
const program2 = ZkProgram({
name: "program2",
publicInput: Field,
methods: {
check: {
privateInputs: [ProgramProof, VerificationKey],
async method(
publicInput: Field,
proof: ProgramProof,
vk: VerificationKey
) {
proof.verify(vk);
proof.publicInput.assertEquals(publicInput);
},
},
},
});
export class Contract extends SmartContract {
@state(Field) value = State<Field>();
@method async setValue(proof: ProgramProof, vk: VerificationKey) {
proof.verify(vk);
this.value.set(proof.publicInput);
}
}
describe("Side loading", () => {
let program1Vk: VerificationKey;
let program2Vk: VerificationKey;
let proof: ProgramProof;
const value = Field(1);
it("should compile", async () => {
const cache: Cache = Cache.FileSystem("./cache");
await Contract.compile({ cache });
program1Vk = (await program1.compile({ cache })).verificationKey;
program2Vk = (await program2.compile({ cache })).verificationKey;
});
it("should prove", async () => {
const program1Proof = await program1.check(value, Field(1));
const program1SideLoadedProof = ProgramProof.fromProof(program1Proof);
const program2Proof = await program2.check(
value,
program1SideLoadedProof,
program1Vk
);
proof = ProgramProof.fromProof(program2Proof);
// Uncomment next line to make the test pass
// proof = ProgramProof.fromProof(program1Proof);
});
it("should deploy SmartContract and set value", async () => {
const network = await Mina.LocalBlockchain();
Mina.setActiveInstance(network);
const sender = network.testAccounts[0];
const appKey = PrivateKey.randomKeypair();
const zkApp = new Contract(appKey.publicKey);
const tx = await Mina.transaction(
{ sender, fee: 100_000_000 },
async () => {
AccountUpdate.fundNewAccount(sender);
await zkApp.deploy();
}
);
await (await tx.sign([sender.key, appKey.privateKey]).send()).wait();
const tx2 = await Mina.transaction(
{ sender, fee: 100_000_000 },
async () => {
// Replace program2Vk with program1Vk to make the test pass
await zkApp.setValue(proof, program2Vk);
}
);
await tx2.prove();
await (await tx2.sign([sender.key]).send()).wait();
expect(zkApp.value.get().toJSON()).toEqual(value.toJSON());
});
}); |
And one more similar error that occurs when I chain ZkPrograms with side-loaded keys is:
Test code: import { describe, expect, it } from "@jest/globals";
import {
PublicKey,
Mina,
PrivateKey,
DynamicProof,
VerificationKey,
ZkProgram,
Field,
SmartContract,
Struct,
method,
AccountUpdate,
state,
State,
UInt32,
DeployArgs,
Bool,
Cache,
Signature,
verify,
} from "o1js";
export class AddProof extends DynamicProof<Field, Field> {
static publicInputType = Field;
static publicOutputType = Field;
static maxProofsVerified = 0 as const;
}
class NFTStateInput extends Struct({
creator: PublicKey,
metadata: Field,
owner: PublicKey,
version: UInt32,
canChangeOwner: Bool,
}) {
static assertEqual(a: NFTStateInput, b: NFTStateInput) {
a.creator.assertEquals(b.creator);
a.metadata.assertEquals(b.metadata);
a.owner.assertEquals(b.owner);
a.version.assertEquals(b.version);
a.canChangeOwner.assertEquals(b.canChangeOwner);
}
}
class NFTStateOutput extends Struct({
metadata: Field,
owner: PublicKey,
}) {}
const nftProgram = ZkProgram({
name: "nftProgram",
publicInput: NFTStateInput,
publicOutput: NFTStateOutput,
methods: {
updateMetadata: {
privateInputs: [Field, PublicKey],
async method(
initialState: NFTStateInput,
metadata: Field,
owner: PublicKey
) {
initialState.owner.assertEquals(owner);
return new NFTStateOutput({
metadata,
owner,
});
},
},
changeOwner: {
privateInputs: [PublicKey], //, Signature
async method(
initialState: NFTStateInput,
newOwner: PublicKey
// https://github.com/o1-labs/o1js/issues/1854
//signature: Signature
) {
// signature
// .verify(initialState.owner, [
// ...NFTStateInput.toFields(initialState),
// ...newOwner.toFields(),
// ])
// .assertTrue();
return new NFTStateOutput({
metadata: initialState.metadata,
owner: newOwner,
});
},
},
// https://github.com/o1-labs/o1js/issues/1854
// Commenting the add method will make the test pass
add: {
privateInputs: [AddProof, VerificationKey],
async method(
initialState: NFTStateInput,
proof: AddProof,
vk: VerificationKey
) {
proof.publicInput.assertEquals(initialState.metadata);
proof.verify(vk);
return new NFTStateOutput({
metadata: proof.publicOutput,
owner: initialState.owner,
});
},
},
},
});
export class NFTProof extends DynamicProof<NFTStateInput, NFTStateOutput> {
static publicInputType = NFTStateInput;
static publicOutputType = NFTStateOutput;
static maxProofsVerified = 0 as const;
}
interface NFTContractDeployParams extends Exclude<DeployArgs, undefined> {
metadata: Field;
owner: PublicKey;
creator: PublicKey;
metadataVerificationKeyHash: Field;
canChangeOwner: Bool;
}
export class NFTContract extends SmartContract {
@state(Field) metadata = State<Field>();
@state(PublicKey) owner = State<PublicKey>();
@state(PublicKey) creator = State<PublicKey>();
@state(UInt32) version = State<UInt32>();
@state(Field) metadataVerificationKeyHash = State<Field>();
@state(Bool) canChangeOwner = State<Bool>();
async deploy(props: NFTContractDeployParams) {
await super.deploy(props);
this.metadata.set(props.metadata);
this.owner.set(props.owner);
this.creator.set(props.creator);
this.metadataVerificationKeyHash.set(props.metadataVerificationKeyHash);
this.version.set(UInt32.from(1));
this.canChangeOwner.set(props.canChangeOwner);
}
@method async updateMetadata(proof: NFTProof, vk: VerificationKey) {
this.metadataVerificationKeyHash
.getAndRequireEquals()
.assertEquals(vk.hash);
proof.verify(vk);
NFTStateInput.assertEqual(
proof.publicInput,
new NFTStateInput({
creator: this.creator.getAndRequireEquals(),
metadata: this.metadata.getAndRequireEquals(),
owner: this.owner.getAndRequireEquals(),
version: this.version.getAndRequireEquals(),
canChangeOwner: this.canChangeOwner.getAndRequireEquals(),
})
);
this.metadata.set(proof.publicOutput.metadata);
this.owner.set(proof.publicOutput.owner);
}
}
const pluginProgram = ZkProgram({
name: "pluginProgram",
publicInput: Field,
publicOutput: Field,
methods: {
add: {
privateInputs: [Field],
async method(a: Field, b: Field) {
return a.add(b);
},
},
},
});
let nftProgramVk: VerificationKey;
let pluginProgramVk: VerificationKey;
const cache: Cache = Cache.FileSystem("./cache");
const owner = PrivateKey.randomKeypair();
const creator = PrivateKey.randomKeypair();
const metadata = Field(1);
const zkAppKey = PrivateKey.randomKeypair();
const nftContract = new NFTContract(zkAppKey.publicKey);
let sender: Mina.TestPublicKey;
describe("NFT with Side loading verification key", () => {
it("should initialize a blockchain", async () => {
const network = await Mina.LocalBlockchain();
Mina.setActiveInstance(network);
sender = network.testAccounts[0];
});
it("should compile", async () => {
await NFTContract.compile({ cache });
nftProgramVk = (await nftProgram.compile({ cache })).verificationKey;
pluginProgramVk = (await pluginProgram.compile({ cache })).verificationKey;
});
it("should deploy a SmartContract", async () => {
const tx = await Mina.transaction(
{ sender, fee: 100_000_000 },
async () => {
AccountUpdate.fundNewAccount(sender);
await nftContract.deploy({
creator: creator.publicKey,
metadata,
owner: owner.publicKey,
metadataVerificationKeyHash: nftProgramVk.hash,
canChangeOwner: Bool(true),
});
}
);
await (await tx.sign([sender.key, zkAppKey.privateKey]).send()).wait();
});
it("should update metadata", async () => {
const newMetadata = Field(7);
const nftInputState = new NFTStateInput({
creator: nftContract.creator.get(),
metadata: nftContract.metadata.get(),
owner: nftContract.owner.get(),
version: nftContract.version.get(),
canChangeOwner: nftContract.canChangeOwner.get(),
});
const proof = await nftProgram.updateMetadata(
nftInputState,
newMetadata,
nftContract.owner.get()
);
const metadataProof = NFTProof.fromProof(proof);
const tx = await Mina.transaction(
{ sender, fee: 100_000_000 },
async () => {
await nftContract.updateMetadata(metadataProof, nftProgramVk);
}
);
await tx.prove();
await (await tx.sign([sender.key]).send()).wait();
const metadata = nftContract.metadata.get();
expect(metadata.toJSON()).toBe(newMetadata.toJSON());
});
}); |
Signature verification uses custom gates, so I think this is a matter of setting the right featureFlags on the DynamicProof |
Thank you! Using It does not help with the other two cases that use chaining of ZkPrograms, but if I do not use side-loading verification keys at all and still chain ZkPrograms, everything works. |
The error when chaining multiple side loaded zkPrograms together originates in using the wrong In const program1Proof = await program1.check(value, Field(1));
const program1SideLoadedProof = ProgramProof.fromProof(program1Proof);
const program2Proof = await program2.check(
value,
program1SideLoadedProof,
program1Vk
);
proof = ProgramProof.fromProof(program2Proof);
// Uncomment next line to make the test pass
// proof = ProgramProof.fromProof(program1Proof); you repurpose Long story short, introducing another // proof class for the non-recursive zkProgram
class NonRecursiveProof extends DynamicProof<Field, Void> {
static publicInputType = Field;
static publicOutputType = Void;
static maxProofsVerified = 0 as const;
static featureFlags: FeatureFlags = FeatureFlags.allMaybe;
}
// proof class for the zkProgram that verfies the non-recursive Program
class RecusiveProof extends DynamicProof<Field, Void> {
static publicInputType = Field;
static publicOutputType = Void;
static maxProofsVerified = 1 as const;
static featureFlags: FeatureFlags = FeatureFlags.allMaybe;
}
|
Thank you! This solution works. Can I set the According to the comments in the code,
https://github.com/o1-labs/o1js/blob/main/src/lib/proof-system/proof.ts#L186-L187 but when I try to set the
or, if I set the
|
Using in the SmartContract method a proof generated by the ZkProgram that verifies the Signature throws the error:
The same test pass if I comment the line in the ZkProgram
or do not use side-loading verification key.
The code to reproduce the issue:
The text was updated successfully, but these errors were encountered: