o1-labs / o1js

TypeScript framework for zk-SNARKs and zkApps
https://docs.minaprotocol.com/en/zkapps/how-to-write-a-zkapp
Apache License 2.0
495 stars 108 forks source link

It is impossible to set zkAppUri inside SmartContract’s method #1226

Open dfstio opened 10 months ago

dfstio commented 10 months ago

Although it is possible to set the verification key inside the SmartContract’s method by passing the variable of the type VerificationKey to the method, it is not possible to set zkAppUri inside the SmartContract’s method by passing the variable of the type Types.ZkappUri to the method.

Error thrown: “Argument 1 of method setURI is not a provable type: function Object() { [native code] }”

Both VerificationKey and Types.ZkappUri internally contains string and Field, but Types.ZkappUri is not provable. Also, account.zkappUri.set accepts only string, while account.verificationKey.set accepts hash and string:

type ZkappUri = {
    data: string;
    hash: Field;
}

(method) account.zkappUri.set(value: string): void

class VerificationKey extends Struct({
  ...provable({ data: String, hash: Field }),
  toJSON({ data }: { data: string }) {
    return data;
  },
}) {}

(method) account.verificationKey.set(value: {
    data: string;
    hash: Field;
}): void

The code to reproduce the error:

import { describe, expect, it } from "@jest/globals";
import {
  Field,
  SmartContract,
  method,
  VerificationKey,
  Types,
  DeployArgs,
  Permissions,
} from "o1js";

jest.setTimeout(1000 * 60 * 60); // 1 hour

describe("Compile a contract", () => {
  it("should compile a contract VK", async () => {
    class VK extends SmartContract {
      deploy(args: DeployArgs) {
        super.deploy(args);
        this.account.permissions.set({
          ...Permissions.default(),
          setDelegate: Permissions.proof(),
          setPermissions: Permissions.proof(),
          setVerificationKey: Permissions.proof(),
          setZkappUri: Permissions.proof(),
          setTokenSymbol: Permissions.proof(),
          incrementNonce: Permissions.proof(),
          setVotingFor: Permissions.proof(),
          setTiming: Permissions.proof(),
        });
        this.emitEvent("mint", Field(0));
      }

      @method setVK(key: VerificationKey) {
        this.account.verificationKey.set(key);
      }
    }
    console.time("compiled");
    console.log("Compiling VK");
    await VK.compile();
    console.timeEnd("compiled");
  });

  it("should compile a contract URI", async () => {
    class URI extends SmartContract {
      deploy(args: DeployArgs) {
        super.deploy(args);
        this.account.permissions.set({
          ...Permissions.default(),
          setDelegate: Permissions.proof(),
          setPermissions: Permissions.proof(),
          setVerificationKey: Permissions.proof(),
          setZkappUri: Permissions.proof(),
          setTokenSymbol: Permissions.proof(),
          incrementNonce: Permissions.proof(),
          setVotingFor: Permissions.proof(),
          setTiming: Permissions.proof(),
        });
        this.emitEvent("mint", Field(0));
      }

      @method setURI(uri: Types.ZkappUri) {
        this.account.zkappUri.set(uri.data);
      }
    }
    console.time("compiled");
    console.log("Compiling URI");
    await URI.compile();
    console.timeEnd("compiled");
  });
});
mitschabaude commented 10 months ago

@dfstio the problem isn't that Types.ZkappUri is not provable, but that it's not a class. The @method decorator tries to automatically find a class from the type arguments. It works for VerificationKey because it extends a Struct.

You can pass a plain string to your method by wrapping it in a Struct, like

class MyString extends Struct({ string: String }) {}

@method myMethod(s: MyString) {
  account.zkappUri.set(s.string);
dfstio commented 10 months ago

@mitschabaude It gives the error: Error when proving URI.setURI() the permutation was not constructed correctly: final value (as described in https://github.com/o1-labs/o1js/issues/130)

import { describe, expect, it } from "@jest/globals";
import {
  SmartContract,
  method,
  Struct,
  DeployArgs,
  Permissions,
  Mina,
  PrivateKey,
  fetchAccount,
  AccountUpdate,
  PublicKey,
} from "o1js";

// Private key of the deployer:
import { DEPLOYER } from "../env.json";

// True - local blockchain, false - Berkeley
const useLocalBlockchain: boolean = true;

const MINAURL = "https://proxy.berkeley.minaexplorer.com/graphql";
const ARCHIVEURL = "https://archive.berkeley.minaexplorer.com";
jest.setTimeout(1000 * 60 * 60 * 10); // 10 hours
const transactionFee = 150_000_000;
let deployer: PrivateKey | undefined = undefined;
let zkAppPublicKey: PublicKey | undefined = undefined;

class URIString extends Struct({ uri: String }) {}

class URI extends SmartContract {
  deploy(args: DeployArgs) {
    super.deploy(args);
    this.account.permissions.set({
      ...Permissions.default(),
      setDelegate: Permissions.proof(),
      setPermissions: Permissions.proof(),
      setVerificationKey: Permissions.proof(),
      setZkappUri: Permissions.proof(),
      setTokenSymbol: Permissions.proof(),
      incrementNonce: Permissions.proof(),
      setVotingFor: Permissions.proof(),
      setTiming: Permissions.proof(),
    });
  }

  @method setURI(zkUri: URIString) {
    this.account.zkappUri.set(zkUri.uri);
  }
}

beforeAll(async () => {
  if (useLocalBlockchain) {
    const Local = Mina.LocalBlockchain({ proofsEnabled: true });
    Mina.setActiveInstance(Local);
    const { privateKey } = Local.testAccounts[0];
    deployer = privateKey;
  } else {
    const network = Mina.Network({
      mina: MINAURL,
      archive: ARCHIVEURL,
    });
    Mina.setActiveInstance(network);
    deployer = PrivateKey.fromBase58(DEPLOYER);
  }
  console.time("compiled");
  console.log("Compiling URI");
  await URI.compile();
  console.timeEnd("compiled");
});

describe("Set and change zkAppUri", () => {
  it("should deploy the contract and set zkAppUri", async () => {
    expect(deployer).not.toBeUndefined();
    if (deployer === undefined) return;

    const sender = deployer.toPublicKey();
    const zkAppPrivateKey = PrivateKey.random();
    zkAppPublicKey = zkAppPrivateKey.toPublicKey();
    await fetchAccount({ publicKey: sender });
    await fetchAccount({ publicKey: zkAppPublicKey });

    const zkApp = new URI(zkAppPublicKey);
    const transaction = await Mina.transaction(
      { sender, fee: transactionFee },
      () => {
        AccountUpdate.fundNewAccount(sender);
        zkApp.deploy({});
        zkApp.account.zkappUri.set("https://zkapp1.io");
      }
    );
    await transaction.prove();
    transaction.sign([deployer, zkAppPrivateKey]);
    const tx = await transaction.send();
    console.log(
      `deploying the URI contract to an address ${zkAppPublicKey.toBase58()}
using the deployer with public key ${sender.toBase58()}:
`,
      transaction.toPretty()
    );
    if (!useLocalBlockchain) {
      await tx.wait({ maxAttempts: 120, interval: 60000 });
    }
    const account = await fetchAccount({ publicKey: zkAppPublicKey });
    const uri = account.account?.zkapp?.zkappUri;
    if (!useLocalBlockchain) {
      expect(uri).not.toBeUndefined();
      expect(uri).toBe("https://zkapp1.io");
    }
    //  {data: uri.data, hash: uri.hash} as MetadataURI;
  });

  it("should change zkAppUri", async () => {
    expect(deployer).not.toBeUndefined();
    if (deployer === undefined) return;
    expect(zkAppPublicKey).not.toBeUndefined();
    if (zkAppPublicKey === undefined) return;

    const sender = deployer.toPublicKey();
    await fetchAccount({ publicKey: sender });
    await fetchAccount({ publicKey: zkAppPublicKey });

    const zkApp = new URI(zkAppPublicKey);
    const zkUri: URIString = { uri: String("https://zkapp2.io") } as URIString;

    const transaction = await Mina.transaction(
      { sender, fee: transactionFee },
      () => {
        zkApp.setURI(zkUri);
      }
    );
    await transaction.prove();
    transaction.sign([deployer]);
    const tx = await transaction.send();
    console.log(`Changing zkAppUri`, transaction.toPretty());
    if (!useLocalBlockchain) {
      await tx.wait({ maxAttempts: 120, interval: 60000 });
    }
    const account = await fetchAccount({ publicKey: zkAppPublicKey });
    const uri = account.account?.zkapp?.zkappUri;
    if (!useLocalBlockchain) {
      expect(uri).not.toBeUndefined();
      expect(uri).toBe("https://zkapp2.io");
    }
  });
});
dfstio commented 10 months ago

Setting zkAppUri for token accounts Account(address, tokenId) seems only possible inside the token owner @method, and this issue blocks this possibility.