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 and finite field operations (isSquare, sqrt) #287

Merged
merged 7 commits into from
Jul 31, 2024

Conversation

MartinMinkov
Copy link
Contributor

@MartinMinkov MartinMinkov commented Jul 22, 2024

Description

🔗 o1js PR: o1-labs/o1js#1765

This pull request addresses issues and has a slight optimization change in the finite field operations.

The following tasks were accomplished:

  1. Fixed isSquare and sqrt methods to ensure input is taken modulo p
  2. Optimized the equal method to avoid unnecessary modulo operations

Additional Information

These changes ensure that the finite field operations work correctly for all input values and improve performance by reducing unnecessary computations. The optimization in the equal method is particularly beneficial for frequently used operations.

It also addresses the issue found in the audit where with this code:

import { createForeignField } from 'o1js';

class Field17 extends createForeignField(17n) {}

// This works fine
console.log(Field17.Bigint.sqrt(0n));
// this runs forever  normally --- with the changes it prints 0n
console.log(Field17.Bigint.sqrt(17n));

@MartinMinkov MartinMinkov changed the title feat(finite-field): add canonicalize function Add canonicalize function for finite-field.ts Jul 22, 2024
@MartinMinkov MartinMinkov marked this pull request as ready for review July 22, 2024 21:15
Copy link
Contributor

@mitschabaude mitschabaude left a comment

Choose a reason for hiding this comment

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

some comments!

comment on performance: canonicalize() is more efficient in the case its input is already canonical (in that case, it avoids the mod() call thanks to the inequality checks)
it's less efficient in the case the input needs to be reduced.

overall, I think this trade-off is worth it, so I support changing canonicalize() to mod() all over the field module even though they do the same!

},
fromBigint(x: bigint) {
return mod(x, p);
return canonicalize(x);
},
rot(
Copy link
Contributor

@mitschabaude mitschabaude Jul 23, 2024

Choose a reason for hiding this comment

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

rot(), leftShift() and rightShift() are not finite field operations, and reducing modulo the field size makes no sense here -- so please remove canonicalize() in these functions (they shouldn't have been put into finite-field.ts in the first place)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed! 145a928

},
power(x: bigint, n: bigint) {
return power(x, n, p);
return canonicalize(power(x, n, p));
Copy link
Contributor

Choose a reason for hiding this comment

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

power already returns a reduced result, can you remove canonicalize() here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

},
isSquare(x: bigint) {
return isSquare(x, p);
return isSquare(canonicalize(x), p);
Copy link
Contributor

Choose a reason for hiding this comment

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

this step should be part of the isSquare() function -- can you do mod(., p) on the input there and remove canonicalize() here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed! 145a928

Comment on lines 320 to 321
const result = sqrt(
canonicalize(x),
Copy link
Contributor

Choose a reason for hiding this comment

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

this step seems to fix a bug and so is an important part of the sqrt() function -- can you do mod(., p) on the input there and remove canonicalize() here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed! 145a928

twoadicRoot,
twoadicity
);
return result !== undefined ? canonicalize(result) : undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

the result returned by sqrt() is already reduced so please remove canonicalize() here

(i.e. you can return sqrt(...) directly)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

},
dot(x: bigint[], y: bigint[]) {
let z = 0n;
let n = x.length;
for (let i = 0; i < n; i++) {
z += x[i] * y[i];
z += canonicalize(x[i]) * canonicalize(y[i]);
Copy link
Contributor

Choose a reason for hiding this comment

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

this is not needed, it's handled by the final call to mod() / canonicalize() before returning

(in fact, I introduced the dot() method precisely to save calls to mod() compared to just using multiplication and addition, for performance of Poseidon)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

},
equal(x: bigint, y: bigint) {
return mod(x - y, p) === 0n;
return canonicalize(x) === canonicalize(y);
Copy link
Contributor

Choose a reason for hiding this comment

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

at first glance I thought this is less efficient than the original, but since inputs are typically canonical, it should actually be more efficient since we never have to mod()!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the canonical function and just replaced with inline checks to mod here! 145a928

},
random() {
return randomField(p, sizeInBytes, hiBitMask);
return canonicalize(randomField(p, sizeInBytes, hiBitMask));
Copy link
Contributor

Choose a reason for hiding this comment

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

randomField() already returns a canonical result, so please remove canonicalize() here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

* {@link sqrt()}, and others. By using this function, we ensure consistent behavior
* across all field operations regardless of the input's initial representation.
*/
function canonicalize(x: bigint) {
Copy link
Member

@Trivo25 Trivo25 Jul 23, 2024

Choose a reason for hiding this comment

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

I actually don't see how this is different to mod(x, p) (except performance in some particular cases) and why we should replacemod with this new function - we could simply check inputs (and outputs) by calling mod(x, p) to assert that they are canonical, like this:

- return sqrt(x, p, oddFactor, twoadicRoot, twoadicity);
+ return sqrt(mod(x, p), p, oddFactor, twoadicRoot, twoadicity);

and use canonicalize for performance critical parts

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My original thinking was that by introducing checks in certain methods and not others, it could lead to a similar situation in the future. By blanket applying a function to alter the output to all of these methods, we'd have a way similar way for all methods to move forward on. This turns out not to be a favourable choice though!

I have reverted the changes and just applied mod to the inputs of the offending functions that cause the bugs outlined in the script above.

@MartinMinkov MartinMinkov changed the title Add canonicalize function for finite-field.ts Fix and finite field operations (isSquare, sqrt) Jul 23, 2024
fix(finite-field): optimize equal method by avoiding modulo operation when possible
refactor(finite-field): use more descriptive variable names in equal method

The isSquare and sqrt methods were not taking the input modulo p, which could lead to incorrect results for inputs outside the field. The equal method was performing a modulo operation unnecessarily in some cases, which could be avoided by first checking if the inputs are already in the correct range. The variable names in the equal method were also improved for clarity.
… and isSquare methods

The commit adds two new test cases to the finite field unit tests:

1. It asserts that calling `sqrt` with a non-canonical zero (i.e., `p`) should return the same result as calling it with a canonical zero (`0n`). This ensures that the `sqrt` method handles non-canonical zero inputs correctly.

2. It asserts that calling `isSquare` with a non-canonical zero (`p`) should return the same result as calling it with a canonical zero (`0n`). This ensures that the `isSquare` method treats non-canonical zero inputs the same way as canonical zero.

These tests were added to improve the robustness of the finite field implementation by verifying that it correctly handles non-canonical zero values, which are equivalent to the canonical zero value in the field.
@@ -317,7 +317,9 @@ function createField(
return mod(z, p);
},
equal(x: bigint, y: bigint) {
return mod(x - y, p) === 0n;
let x_ = x >= 0n && x < p ? x : mod(x, p);
Copy link
Member

Choose a reason for hiding this comment

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

I think I am okay with this, but let's add a comment here explaining what and why we do it this particular way, i can imagine it might seem a little weird to the naked eye

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed here! 71f2e69

Comment on lines 302 to 306
isSquare(x: bigint) {
return isSquare(x, p);
return isSquare(mod(x, p), p);
},
sqrt(x: bigint) {
return sqrt(x, p, oddFactor, twoadicRoot, twoadicity);
return sqrt(mod(x, p), p, oddFactor, twoadicRoot, twoadicity);
Copy link
Contributor

@mitschabaude mitschabaude Jul 30, 2024

Choose a reason for hiding this comment

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

so what I meant in my other review here is that I'd like those changes to be made to the original sqrt and isSquare functions defined at the top-level of this file. just so that we don't have two versions of sqrt(), one with a bug and another that fixes the bug

Copy link
Contributor

Choose a reason for hiding this comment

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

I know that currently the original sqrt() / isSquare() are not even exported directly, but other basic functions are, and if someone decides to export sqrt() directly in the future they probably won't think of fixing its bug

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed here! cc3f632

Copy link
Contributor

@mitschabaude mitschabaude left a comment

Choose a reason for hiding this comment

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

the following two methods also need mod() on their inputs to always behave correctly:

  • negate() (can just do mod(-x, p))
  • isEven()

…stead of p - x for improved readability and consistency with other functions

fix(finite-field): use mod in isEven function to ensure the result is always within the field range and avoid potential issues with large numbers
…g input to ensure it's in the field

The changes in the `sqrt` and `isSquare` functions were made to ensure that the input value is always within the finite field before performing any operations. This is done by modding the input value by the field's prime modulus `p` at the beginning of each function.

The reason for this change is to handle cases where the input value might be outside the field, which could lead to incorrect results or even errors. By modding the input, we guarantee that all operations are performed on values within the field, ensuring the correctness of the results.

This refactoring improves the robustness and reliability of the `sqrt` and `isSquare` functions, making them more consistent with the expected behavior of finite field arithmetic.
- Assert that negating non-canonical zero (p) returns canonical zero (0n)
- Assert that non-canonical zero (p) is considered even

These additional test cases ensure that the finite field implementation handles non-canonical zero correctly by treating it as equivalent to the canonical zero representation.
…the number modulo p before checking the least significant bit

This fixes a bug where the isEven function would incorrectly classify numbers greater than the field size as even. By first reducing the number modulo p, we ensure that the least significant bit correctly determines the parity of the number within the field.
Copy link
Contributor

@mitschabaude mitschabaude left a comment

Choose a reason for hiding this comment

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

sweet!

@MartinMinkov MartinMinkov merged commit 4ff31bc into main Jul 31, 2024
1 check passed
@MartinMinkov MartinMinkov deleted the audit/field-curve-canon branch July 31, 2024 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants