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
493 stars 107 forks source link

Accessing two token accounts in one SmartContract's @method causes FieldVector.get(): Index out of bounds error or get() function returns incorrect token balance value #1245

Open dfstio opened 9 months ago

dfstio commented 9 months ago

When @method accesses one token Account(address, tokenId), everything works as expected. When trying to access two token accounts, the following error appears while proving:

● MinaNFT contract › should issue badges, iteration 1

    FieldVector.get(): Index out of bounds, got 314229/314229

      at get (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:2119:13)
      at external_values (src/lib/crypto/kimchi_backend/pasta/vesta_based_plonk.ml:95:33)
      at ../../../../../../workspace_root/src/lib/crypto/kimchi_backend/common/plonk_constraint_system.ml:990:44
      at iteri$1 (ocaml/base/array0.ml:50:18)
      at _oy4_ (src/lib/crypto/kimchi_backend/common/plonk_constraint_system.ml:984:9)
      at ../../../../../../home/gregor/.opam/4.14.0/lib/base/list.ml:544:8
      at fold_left$0 (ocaml/ocaml/list.ml:121:25)
      at fold_left$2 (ocaml/base/list0.ml:21:23)
      at iteri$2 (ocaml/base/list.ml:543:6)
      at compute_witness (src/lib/crypto/kimchi_backend/common/plonk_constraint_system.ml:983:5)
      at create_async (src/lib/crypto/kimchi_backend/pasta/vesta_based_plonk.ml:117:7)
      at create_proof (src/lib/pickles/step.ml:816:27)
      at ../../../../../../workspace_root/src/lib/pickles/step.ml:822:27
      at ../../../../../../workspace_root/src/lib/snarky/src/base/runners.ml:351:13
      at ../../../../../../workspace_root/src/lib/snarky/src/base/runners.ml:305:34
      at ../../../../../../workspace_root/src/lib/snarky/src/base/snark0.ml:1302:19
      at finalize_is_running (src/lib/snarky/src/base/snark0.ml:1272:15)
      at generate_witness_conv (src/lib/snarky/src/base/snark0.ml:1301:7)
      at prove (src/lib/snarkyjs/src/bindings/ocaml/lib/pickles_bindings.ml:638:7)
      at .yarn/cache/o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:13072:59
      at withThreadPool (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:3168:20)
      at prettifyStacktracePromise (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:2867:12)
      at .yarn/cache/o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:16552:14
      at Object.run (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:13311:16)
      at createZkappProof (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:16545:21)
      at addProof (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:16538:15)
      at addMissingProofs (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:16510:42)
      at Object.prove (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:16836:38)
      at MinaNFTBadge.issue (src/minanftbadge.ts:277:5)
      at Object.<anonymous> (tests/update.test.ts:181:20)
dfstio commented 9 months ago

If method1, accessing token account A, calls method2, accessing token account B, the get function gives 0 for the balance of token account B even though the balance is not 0:

 ● MinaNFT contract › escrow should transfer NFT and funds

    Field.assertEquals(): 0 != 1000000000

       96 |     const account = Account(address, this.token.id);
       97 |     const tokenBalance = account.balance.getAndAssertEquals();
    >  98 |     tokenBalance.assertEquals(UInt64.from(1_000_000_000));
          |                  ^
       99 |   }

 at _Field.assertEquals (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:4671:17)
      at assertEqualImplicit (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:6466:12)
      at Object.assertEqual (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:6456:12)
      at _UInt64.assertEquals (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:7132:14)
      at MinaNFTNameServiceContract.isNFT (src/contract/names.ts:98:18)
      at MinaNFTNameServiceContract.transfer (src/contract/names.ts:199:10)
      at runCalledContract (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:14514:29)
      at .yarn/cache/o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:6440:21
      at run_prover (src/lib/snarky/src/base/snark0.ml:1150:17)
      at run$3 (src/lib/snarky/src/base/as_prover0.ml:15:17)
      at run$4 (src/lib/snarky/src/base/as_prover0.ml:74:18)
      at ../../../../../../workspace_root/src/lib/snarky/src/base/checked_runner.ml:260:15
      at ../../../../../../workspace_root/src/lib/snarky/src/base/checked_runner.ml:54:30
      at exists (src/lib/snarky/src/base/snark0.ml:1213:11)
      at exists$12 (src/lib/snarkyjs/src/bindings/ocaml/lib/snarky_bindings.ml:17:15)
      at Function.Class.<computed> [as exists] (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:3413:52)
      at Object.witness (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:6439:35)
      at Function.witness (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:16063:21)
      at MinaNFTNameServiceContract.wrappedMethod (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:14541:78)
      at Escrow.transfer (src/plugins/escrow.ts:81:13)
      at Escrow.wrappedMethod (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:14446:30)
      at src/escrow.ts:203:15
      at .yarn/cache/o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:16769:13
      at ../../../../../../workspace_root/src/lib/snarkyjs/src/bindings/ocaml/lib/snarky_bindings.ml:38:11
      at ../../../../../../workspace_root/src/lib/snarky/src/base/snark0.ml:1315:40
      at mark_active (src/lib/snarky/src/base/snark0.ml:1167:19)
      at _piO_ (src/lib/snarky/src/base/snark0.ml:1314:17)
      at as_stateful (src/lib/snarky/src/base/snark0.ml:755:15)
      at run_and_check_exn (src/lib/snarky/src/base/runners.ml:115:16)
      at ../../../../../../workspace_root/src/lib/snarky/src/base/snark0.ml:1313:13
      at finalize_is_running (src/lib/snarky/src/base/snark0.ml:1272:15)
      at run_and_check_exn (src/lib/snarky/src/base/snark0.ml:1311:7)
      at Object.runUnchecked (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:4163:16)
      at createTransaction (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:16768:20)
      at Object.transaction (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:17208:16)
      at Object.transaction (o1js-npm-0.14.1-6dd1d545d0-5ce9973c48.zip/node_modules/o1js/dist/node/index.cjs:17321:27)
      at MinaNFTEscrow.transfer (src/escrow.ts:200:36)
      at Object.<anonymous> (tests/escrow.test.ts:278:18)

In reality, the balance is not 0; it is 1000000000, as shown by several checks made before calling transfer() The bug exists while using testworld2; on the local blockchain, the function get() returns the correct result.

dfstio commented 8 months ago

There is also a discussion on the hierarchy of Token accounts (i.e., many nested token accounts) on the discord that can be affected by this behavior: https://discord.com/channels/484437221055922177/1171202435663007804

dfstio commented 6 months ago

Today, I checked my old code that throws this error, and the error is still there. I went to debug it and found out the error is being thrown when I use getAndRequireEquals() on token accounts:

@method checkFail(address: PublicKey, tokenId: Field) {
    const version = this.version.getAndRequireEquals();
    version.assertEquals(Field(1));
    const token = new TokenAccount(address, tokenId);
    // If you comment next two lines, the test will pass
    const value = token.value.getAndRequireEquals();
    value.assertEquals(Field(1));
  }

When I use get(), tests pass:

 @method checkPass(address: PublicKey, tokenId: Field) {
    const version = this.version.getAndRequireEquals();
    version.assertEquals(Field(1));
    const token = new TokenAccount(address, tokenId);
    const value = token.value.get();
    value.assertEquals(Field(1));
  }

The test code I use: https://github.com/dfstio/minanft-lib/blob/token-accounts/experimental/issue_token_accounts.test.ts

dfstio commented 1 month ago

The discord discussion: https://discord.com/channels/484437221055922177/1261748801371373729/1262142233973362770