Skip to content
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

fix: Broadcasting unconstrained function with empty sibling #5429

Merged
merged 1 commit into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions yarn-project/circuits.js/scripts/copy-contracts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ set -euo pipefail
mkdir -p ./fixtures

cp "../../noir-projects/noir-contracts/target/benchmarking_contract-Benchmarking.json" ./fixtures/Benchmarking.test.json
cp "../../noir-projects/noir-contracts/target/test_contract-Test.json" ./fixtures/Test.test.json
4 changes: 2 additions & 2 deletions yarn-project/circuits.js/src/contract/artifact_hash.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { getSampleContractArtifact } from '../tests/fixtures.js';
import { getBenchmarkContractArtifact } from '../tests/fixtures.js';
import { computeArtifactHash } from './artifact_hash.js';

describe('ArtifactHash', () => {
it('calculates the artifact hash', () => {
const artifact = getSampleContractArtifact();
const artifact = getBenchmarkContractArtifact();
expect(computeArtifactHash(artifact).toString()).toMatchInlineSnapshot(
`"0x19dcd971117d72ceed658023cf16036d912de56c75a54da414d2d6bd645c99f2"`,
);
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/circuits.js/src/contract/contract_class.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { FunctionSelector, FunctionType } from '@aztec/foundation/abi';
import { Fr } from '@aztec/foundation/fields';

import { getSampleContractArtifact } from '../tests/fixtures.js';
import { getBenchmarkContractArtifact } from '../tests/fixtures.js';
import { getContractClassFromArtifact } from './contract_class.js';

describe('ContractClass', () => {
it('creates a contract class from a contract compilation artifact', () => {
const artifact = getSampleContractArtifact();
const artifact = getBenchmarkContractArtifact();
const contractClass = getContractClassFromArtifact({
...artifact,
artifactHash: Fr.fromString('0x1234'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { FunctionSelector } from '@aztec/foundation/abi';
import { randomBytes } from '@aztec/foundation/crypto';
import { Fr } from '@aztec/foundation/fields';
import { Tuple } from '@aztec/foundation/serialize';
import { setupCustomSnapshotSerializers } from '@aztec/foundation/testing';

import { getSampleUnconstrainedFunctionBroadcastedEventPayload } from '../../tests/fixtures.js';
import { UnconstrainedFunctionBroadcastedEvent } from './unconstrained_function_broadcasted_event.js';
import {
BroadcastedUnconstrainedFunction,
UnconstrainedFunctionBroadcastedEvent,
} from './unconstrained_function_broadcasted_event.js';

describe('UnconstrainedFunctionBroadcastedEvent', () => {
beforeAll(() => setupCustomSnapshotSerializers(expect));
Expand All @@ -10,4 +17,18 @@ describe('UnconstrainedFunctionBroadcastedEvent', () => {
const event = UnconstrainedFunctionBroadcastedEvent.fromLogData(data);
expect(event).toMatchSnapshot();
});

it('filters out zero-elements at the end of the artifcat tree sibling path', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it('filters out zero-elements at the end of the artifcat tree sibling path', () => {
it('filters out zero-elements at the end of the artifact tree sibling path', () => {

const siblingPath: Tuple<Fr, 5> = [Fr.ZERO, new Fr(1), Fr.ZERO, new Fr(2), Fr.ZERO];
const event = new UnconstrainedFunctionBroadcastedEvent(
Fr.random(),
Fr.random(),
Fr.random(),
siblingPath,
0,
new BroadcastedUnconstrainedFunction(FunctionSelector.random(), Fr.random(), randomBytes(32)),
);
const filtered = event.toFunctionWithMembershipProof().artifactTreeSiblingPath;
expect(filtered).toEqual([Fr.ZERO, new Fr(1), Fr.ZERO, new Fr(2)]);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FunctionSelector, bufferFromFields } from '@aztec/foundation/abi';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { toBigIntBE } from '@aztec/foundation/bigint-buffer';
import { removeArrayPaddingEnd } from '@aztec/foundation/collection';
import { Fr } from '@aztec/foundation/fields';
import { BufferReader, Tuple } from '@aztec/foundation/serialize';
import { UnconstrainedFunction, UnconstrainedFunctionWithMembershipProof } from '@aztec/types/contracts';
Expand Down Expand Up @@ -85,13 +86,18 @@ export class UnconstrainedFunctionBroadcastedEvent {
}

toFunctionWithMembershipProof(): UnconstrainedFunctionWithMembershipProof {
// We should be able to safely remove the zero elements that pad the variable-length sibling path,
// since a sibling with value zero can only occur on the tree leaves, so the sibling path will never end
// in a zero. The only exception is a tree with depth 2 with one non-zero leaf, where the sibling path would
// be a single zero element, but in that case the artifact tree should be just the single leaf.
const artifactTreeSiblingPath = removeArrayPaddingEnd(this.artifactFunctionTreeSiblingPath, Fr.isZero);
Copy link
Collaborator Author

@spalladino spalladino Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To the reviewer: this is the only real logic change in this PR

return {
...this.unconstrainedFunction,
bytecode: this.unconstrainedFunction.bytecode,
functionMetadataHash: this.unconstrainedFunction.metadataHash,
artifactMetadataHash: this.artifactMetadataHash,
privateFunctionsArtifactTreeRoot: this.privateFunctionsArtifactTreeRoot,
artifactTreeSiblingPath: this.artifactFunctionTreeSiblingPath.filter(fr => !fr.isZero()),
artifactTreeSiblingPath,
artifactTreeLeafIndex: this.artifactFunctionTreeLeafIndex,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ContractArtifact, FunctionArtifact, FunctionSelector, FunctionType } fr
import { Fr } from '@aztec/foundation/fields';
import { ContractClass } from '@aztec/types/contracts';

import { getSampleContractArtifact } from '../tests/fixtures.js';
import { getBenchmarkContractArtifact } from '../tests/fixtures.js';
import { computeVerificationKeyHash, getContractClassFromArtifact } from './contract_class.js';
import { ContractClassIdPreimage } from './contract_class_id.js';
import {
Expand All @@ -18,7 +18,7 @@ describe('private_function_membership_proof', () => {
let selector: FunctionSelector;

beforeAll(() => {
artifact = getSampleContractArtifact();
artifact = getBenchmarkContractArtifact();
contractClass = getContractClassFromArtifact(artifact);
privateFunction = artifact.functions.findLast(fn => fn.functionType === FunctionType.SECRET)!;
vkHash = computeVerificationKeyHash(privateFunction.verificationKey!);
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/circuits.js/src/contract/public_bytecode.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { ContractArtifact } from '@aztec/foundation/abi';

import { getSampleContractArtifact } from '../tests/fixtures.js';
import { getBenchmarkContractArtifact } from '../tests/fixtures.js';
import { getContractClassFromArtifact } from './contract_class.js';
import { packBytecode, unpackBytecode } from './public_bytecode.js';

describe('PublicBytecode', () => {
let artifact: ContractArtifact;
beforeAll(() => {
artifact = getSampleContractArtifact();
artifact = getBenchmarkContractArtifact();
});

it('packs and unpacks public bytecode', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ContractArtifact, FunctionArtifact, FunctionSelector, FunctionType } fr
import { Fr } from '@aztec/foundation/fields';
import { ContractClass } from '@aztec/types/contracts';

import { getSampleContractArtifact } from '../tests/fixtures.js';
import { getTestContractArtifact } from '../tests/fixtures.js';
import { getContractClassFromArtifact } from './contract_class.js';
import { ContractClassIdPreimage } from './contract_class_id.js';
import {
Expand All @@ -17,16 +17,34 @@ describe('unconstrained_function_membership_proof', () => {
let vkHash: Fr;
let selector: FunctionSelector;

beforeAll(() => {
artifact = getSampleContractArtifact();
beforeEach(() => {
artifact = getTestContractArtifact();
contractClass = getContractClassFromArtifact(artifact);
unconstrainedFunction = artifact.functions.findLast(fn => fn.functionType === FunctionType.UNCONSTRAINED)!;
selector = FunctionSelector.fromNameAndParameters(unconstrainedFunction);
});

const isUnconstrained = (fn: { functionType: FunctionType }) => fn.functionType === FunctionType.UNCONSTRAINED;

it('computes and verifies a proof', () => {
expect(unconstrainedFunction).toBeDefined();
const proof = createUnconstrainedFunctionMembershipProof(selector, artifact);
const fn = { ...unconstrainedFunction, ...proof, selector };
expect(isValidUnconstrainedFunctionMembershipProof(fn, contractClass)).toBeTruthy();
});

it('handles a contract with a single function', () => {
// Remove all unconstrained functions from the contract but one
const unconstrainedFns = artifact.functions.filter(isUnconstrained);
artifact.functions = artifact.functions.filter(fn => !isUnconstrained(fn) || fn === unconstrainedFns[0]);
expect(artifact.functions.filter(isUnconstrained).length).toBe(1);

const unconstrainedFunction = unconstrainedFns[0];
const proof = createUnconstrainedFunctionMembershipProof(selector, artifact);
expect(proof.artifactTreeSiblingPath.length).toBe(0);

const fn = { ...unconstrainedFunction, ...proof, selector };
const contractClass = getContractClassFromArtifact(artifact);
expect(isValidUnconstrainedFunctionMembershipProof(fn, contractClass)).toBeTruthy();
});

Expand Down
9 changes: 8 additions & 1 deletion yarn-project/circuits.js/src/tests/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';

// Copied from the build output for the contract `Benchmarking` in noir-contracts
export function getSampleContractArtifact(): ContractArtifact {
export function getBenchmarkContractArtifact(): ContractArtifact {
const path = getPathToFixture('Benchmarking.test.json');
const content = JSON.parse(readFileSync(path).toString()) as NoirCompiledContract;
return loadContractArtifact(content);
}

// Copied from the build output for the contract `Benchmarking` in noir-contracts
export function getTestContractArtifact(): ContractArtifact {
const path = getPathToFixture('Benchmarking.test.json');
const content = JSON.parse(readFileSync(path).toString()) as NoirCompiledContract;
return loadContractArtifact(content);
Expand Down
24 changes: 23 additions & 1 deletion yarn-project/foundation/src/collection/array.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { times } from './array.js';
import { removeArrayPaddingEnd, times } from './array.js';

describe('times', () => {
it('should return an array with the result from all executions', () => {
Expand All @@ -11,3 +11,25 @@ describe('times', () => {
expect(result).toEqual([]);
});
});

describe('removeArrayPaddingEnd', () => {
it('removes padding from the end of the array', () => {
expect(removeArrayPaddingEnd([0, 1, 2, 0, 3, 4, 0, 0], i => i === 0)).toEqual([0, 1, 2, 0, 3, 4]);
});

it('does not change array if no padding', () => {
expect(removeArrayPaddingEnd([0, 1, 2, 0, 3, 4], i => i === 0)).toEqual([0, 1, 2, 0, 3, 4]);
});

it('handles no empty items ', () => {
expect(removeArrayPaddingEnd([1, 2, 3, 4], i => i === 0)).toEqual([1, 2, 3, 4]);
});

it('handles empty array', () => {
expect(removeArrayPaddingEnd([], i => i === 0)).toEqual([]);
});

it('handles array with empty items', () => {
expect(removeArrayPaddingEnd([0, 0, 0], i => i === 0)).toEqual([]);
});
});
6 changes: 6 additions & 0 deletions yarn-project/foundation/src/collection/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export function padArrayEnd<T, N extends number>(arr: T[], elem: T, length: N):
return [...arr, ...Array(length - arr.length).fill(elem)] as Tuple<T, N>;
}

/** Removes the right-padding for an array. Does not modify original array. */
export function removeArrayPaddingEnd<T>(arr: T[], isEmpty: (item: T) => boolean): T[] {
const lastNonEmptyIndex = arr.reduce((last, item, i) => (isEmpty(item) ? last : i), -1);
return lastNonEmptyIndex === -1 ? [] : arr.slice(0, lastNonEmptyIndex + 1);
}

/**
* Pads an array to the target length by prepending elements at the beginning. Throws if target length exceeds the input array length. Does not modify the input array.
* @param arr - Array with elements to pad.
Expand Down
Loading