o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
227 lines (191 loc) • 7.99 kB
text/typescript
import { Random, test } from '../../testing/property.js';
import { RandomTransaction } from '../../../mina-signer/src/random-transaction.js';
import { TokenAccountUpdateIterator } from './forest-iterator.js';
import {
AccountUpdate,
AccountUpdateForest,
TokenId,
hashAccountUpdate,
} from '../account-update.js';
import { TypesBigint } from '../../../bindings/mina-transaction/types.js';
import { Pickles, initializeBindings } from '../../../snarky.js';
import {
accountUpdatesToCallForest,
callForestHash,
} from '../../../mina-signer/src/sign-zkapp-command.js';
import assert from 'assert';
import { Field, Bool } from '../../provable/wrapped.js';
import { PublicKey } from '../../provable/crypto/signature.js';
// RANDOM NUMBER GENERATORS for account updates
await initializeBindings();
let [, data, hashMl] = Pickles.dummyVerificationKey();
let dummyVerificationKey = { data, hash: hashMl[1] };
const accountUpdateBigint = Random.map(
RandomTransaction.accountUpdateWithCallDepth,
(a) => {
// fix verification key
if (a.body.update.verificationKey.isSome)
a.body.update.verificationKey.value = dummyVerificationKey;
// ensure that, by default, all account updates are token-accessible
a.body.mayUseToken =
a.body.callDepth === 0
? { parentsOwnToken: true, inheritFromParent: false }
: { parentsOwnToken: false, inheritFromParent: true };
return a;
}
);
const accountUpdate = Random.map(accountUpdateBigint, accountUpdateFromBigint);
const arrayOf = <T>(x: Random<T>) =>
// `reset: true` to start callDepth at 0 for each array
Random.array(x, Random.int(10, 40), { reset: true });
const flatAccountUpdatesBigint = arrayOf(accountUpdateBigint);
const flatAccountUpdates = arrayOf(accountUpdate);
// TESTS
// correctly hashes a call forest
test.custom({ timeBudget: 1000 })(
flatAccountUpdatesBigint,
(flatUpdatesBigint) => {
// reference: bigint callforest hash from mina-signer
let forestBigint = accountUpdatesToCallForest(flatUpdatesBigint);
let expectedHash = callForestHash(forestBigint, 'testnet');
let flatUpdates = flatUpdatesBigint.map(accountUpdateFromBigint);
let forest = AccountUpdateForest.fromFlatArray(flatUpdates);
forest.hash.assertEquals(expectedHash);
}
);
// traverses the top level of a call forest in correct order
// i.e., CallForestArray works
test.custom({ timeBudget: 1000 })(flatAccountUpdates, (flatUpdates) => {
// prepare call forest from flat account updates
let forest =
AccountUpdateForest.fromFlatArray(flatUpdates).startIteratingFromLast();
let updates = flatUpdates.filter((u) => u.body.callDepth === 0);
// step through top-level by calling forest.next() repeatedly
let n = updates.length;
for (let i = 0; i < n; i++) {
let expected = updates[i];
let actual = forest.previous().accountUpdate.unhash();
assertEqual(actual, expected);
}
// doing next() again should return a dummy
let actual = forest.previous().accountUpdate.unhash();
assertEqual(actual, AccountUpdate.dummy());
});
// traverses a call forest in correct depth-first order,
// when no subtrees are skipped
test.custom({ timeBudget: 5000 })(flatAccountUpdates, (flatUpdates) => {
// with default token id, no subtrees will be skipped
let tokenId = TokenId.default;
// prepare forest iterator from flat account updates
let forest = AccountUpdateForest.fromFlatArray(flatUpdates);
let forestIterator = TokenAccountUpdateIterator.create(forest, tokenId);
// step through forest iterator and compare against expected updates
let expectedUpdates = flatUpdates;
let n = flatUpdates.length;
for (let i = 0; i < n; i++) {
let expected = expectedUpdates[i];
let actual = forestIterator.next().accountUpdate;
assertEqual(actual, expected);
}
// doing next() again should return a dummy
let actual = forestIterator.next().accountUpdate;
assertEqual(actual, AccountUpdate.dummy());
});
// correctly skips subtrees for various reasons
// in this test, we make all updates inaccessible by setting the top level to `no` or `inherit`, or to the token owner
// this means we wouldn't need to traverse any update in the whole tree
// but we only notice inaccessibility when we have already traversed the inaccessible update
// so, the result should be that we traverse the top level and nothing else
test.custom({ timeBudget: 5000 })(
flatAccountUpdates,
Random.publicKey,
(flatUpdates, publicKey) => {
// create token owner and derive token id
let tokenOwner = PublicKey.fromObject({
x: Field.from(publicKey.x),
isOdd: Bool(!!publicKey.isOdd),
});
let tokenId = TokenId.derive(tokenOwner);
// make all top-level updates inaccessible
flatUpdates
.filter((u) => u.body.callDepth === 0)
.forEach((u, i) => {
if (i % 3 === 0) {
u.body.mayUseToken = AccountUpdate.MayUseToken.No;
} else if (i % 3 === 1) {
u.body.mayUseToken = AccountUpdate.MayUseToken.InheritFromParent;
} else {
u.body.publicKey = tokenOwner;
u.body.tokenId = TokenId.default;
}
});
// prepare forest iterator from flat account updates
let forest = AccountUpdateForest.fromFlatArray(flatUpdates);
let forestIterator = TokenAccountUpdateIterator.create(forest, tokenId);
// step through forest iterator and compare against expected updates
let expectedUpdates = flatUpdates.filter((u) => u.body.callDepth === 0);
let n = flatUpdates.length;
for (let i = 0; i < n; i++) {
let expected = expectedUpdates[i] ?? AccountUpdate.dummy();
let actual = forestIterator.next().accountUpdate;
assertEqual(actual, expected);
}
}
);
// similar to the test before, but now we make all updates in the second layer inaccessible
// so the iteration should walk through the first and second layer
test.custom({ timeBudget: 5000 })(
flatAccountUpdates,
Random.publicKey,
(flatUpdates, publicKey) => {
// create token owner and derive token id
let tokenOwner = PublicKey.fromObject({
x: Field.from(publicKey.x),
isOdd: Bool(!!publicKey.isOdd),
});
let tokenId = TokenId.derive(tokenOwner);
// make all second-level updates inaccessible
flatUpdates
.filter((u) => u.body.callDepth === 1)
.forEach((u, i) => {
if (i % 3 === 0) {
u.body.mayUseToken = AccountUpdate.MayUseToken.No;
} else if (i % 3 === 1) {
u.body.mayUseToken = AccountUpdate.MayUseToken.ParentsOwnToken;
} else {
u.body.publicKey = tokenOwner;
u.body.tokenId = TokenId.default;
}
});
// prepare forest iterator from flat account updates
let forest = AccountUpdateForest.fromFlatArray(flatUpdates);
let forestIterator = TokenAccountUpdateIterator.create(forest, tokenId);
// step through forest iterator and compare against expected updates
let expectedUpdates = flatUpdates.filter((u) => u.body.callDepth <= 1);
let n = flatUpdates.length;
for (let i = 0; i < n; i++) {
let expected = expectedUpdates[i] ?? AccountUpdate.dummy();
let actual = forestIterator.next().accountUpdate;
assertEqual(actual, expected);
}
}
);
// HELPERS
function accountUpdateFromBigint(a: TypesBigint.AccountUpdate): AccountUpdate {
// bigint to json, then to provable
return AccountUpdate.fromJSON(TypesBigint.AccountUpdate.toJSON(a));
}
function assertEqual(actual: AccountUpdate, expected: AccountUpdate) {
let actualHash = hashAccountUpdate(actual).toBigInt();
let expectedHash = hashAccountUpdate(expected).toBigInt();
assert.deepStrictEqual(actual.body, expected.body);
assert.deepStrictEqual(
actual.authorization.proof,
expected.authorization.proof
);
assert.deepStrictEqual(
actual.authorization.signature,
expected.authorization.signature
);
assert.deepStrictEqual(actualHash, expectedHash);
}