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

Implement to/fromBits in TS, and set max length to 254 bits #1461

Merged
merged 22 commits into from
Mar 12, 2024
Merged
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5dccfb9
Implement to/fromBits in TS, and set max length to 254 bits
jackryanservia Feb 26, 2024
fcf1613
Forgot to change name of fromBits
jackryanservia Feb 26, 2024
157edbf
Fixed mask
jackryanservia Feb 27, 2024
6e4ab72
Swtich import from core.js to prevent dependency cycle
jackryanservia Feb 27, 2024
d4c642f
Fix Field.toBits error message
jackryanservia Feb 27, 2024
c3df5ec
Change field unit test to use 254bit values
jackryanservia Feb 27, 2024
e63f421
Handle constant case manually like before
jackryanservia Mar 4, 2024
c14178a
Add extra 0 to end of Bool.toBits so that Scalar.fromBits is happy
jackryanservia Mar 4, 2024
e706b55
Fix typo
jackryanservia Mar 4, 2024
74fb4f3
Remove unreachable lines
jackryanservia Mar 4, 2024
f9acdbb
Dump vks
jackryanservia Mar 4, 2024
720a9fc
Add explanation for why only 254 bits are allowed in typedoc of fromBits
jackryanservia Mar 4, 2024
d291e18
Better comment in Field unit test
jackryanservia Mar 4, 2024
8509bdc
Refactor Scalar.fromBits method to handle input of less than 255 bits
jackryanservia Mar 4, 2024
bcf9029
Revert "Add extra 0 to end of Bool.toBits so that Scalar.fromBits is …
jackryanservia Mar 4, 2024
da1a7a6
Seal Field.fromBits() return value to avoid proving AST each time ret…
jackryanservia Mar 7, 2024
266b9d1
Add CHANGELOG entry
jackryanservia Mar 7, 2024
08a224b
Merge branch 'main' into fix/field-to-from-254bits
jackryanservia Mar 7, 2024
24aa290
Merge branch 'main' into fix/field-to-from-254bits
jackryanservia Mar 11, 2024
19a3e99
Fix Field import
jackryanservia Mar 11, 2024
e1daa57
Dump vks
jackryanservia Mar 11, 2024
e5cd926
Move CHANGELOG entry to unreleased section
jackryanservia Mar 11, 2024
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
57 changes: 26 additions & 31 deletions src/lib/field.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Snarky, Provable } from '../snarky.js';
import { Snarky } from '../snarky.js';
import { Field as Fp } from '../provable/field-bigint.js';
import { defineBinable } from '../bindings/lib/binable.js';
import type { NonNegativeInteger } from '../bindings/crypto/non-negative.js';
import { asProver, inCheckedComputation } from './provable-context.js';
import { Bool } from './bool.js';
import { assert } from './errors.js';
import { Provable } from './provable.js';

// external API
export { Field };
Expand Down Expand Up @@ -908,34 +909,34 @@ class Field {
* Returns an array of {@link Bool} elements representing [little endian binary representation](https://en.wikipedia.org/wiki/Endianness) of this {@link Field} element.
*
* If you use the optional `length` argument, proves that the field element fits in `length` bits.
* The `length` has to be between 0 and 255 and the method throws if it isn't.
* The `length` has to be between 0 and 254 and the method throws if it isn't.
*
* **Warning**: The cost of this operation in a zk proof depends on the `length` you specify,
* which by default is 255 bits. Prefer to pass a smaller `length` if possible.
* which by default is 254 bits. Prefer to pass a smaller `length` if possible.
*
* @param length - the number of bits to fit the element. If the element does not fit in `length` bits, the functions throws an error.
*
* @return An array of {@link Bool} element representing little endian binary representation of this {@link Field}.
*/
toBits(length?: number) {
if (length !== undefined) checkBitLength('Field.toBits()', length);
if (this.isConstant()) {
let bits = Fp.toBits(this.toBigInt());
if (length !== undefined) {
if (bits.slice(length).some((bit) => bit))
throw Error(`Field.toBits(): ${this} does not fit in ${length} bits`);
return bits.slice(0, length).map((b) => new Bool(b));
}
return bits.map((b) => new Bool(b));
}
let [, ...bits] = Snarky.field.toBits(length ?? Fp.sizeInBits, this.value);
return bits.map((b) => new Bool(b));
toBits(length: number = 254) {
checkBitLength('Field.toBits()', length, 254);
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 understanding is that there isn't a clever way to make this secure (other than just limiting it to 254 bits) because any other method would necessarily involve checking if the value overflowed or not. Does that reasoning check out?

Copy link
Collaborator

Choose a reason for hiding this comment

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

we could provide a secure, 255-bits version by treating the bits as a ForeignField element and asserting that it's less than the field size. That would add some overhead so it's better to not do it by default. It could be a separate method toFullBits() or similar. But not urgent IMO

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This makes sense. I will implement tomorrow if there is time.

let bits = Provable.witness(Provable.Array(Bool, length), () => {
let f = this.toBigInt();
return Array.from(
{ length },
(_, k) => new Bool(!!((f >> BigInt(k)) & 0xffn))
);
});
if (bits.slice(length).some((bit) => bit.toBoolean()))
throw Error(`Field.toBits(): ${this} does not fit in ${length} bits`);
Field.fromBits(bits).assertEquals(this);
jackryanservia marked this conversation as resolved.
Show resolved Hide resolved
return bits;
}

/**
* Convert a bit array into a {@link Field} element using [little endian binary representation](https://en.wikipedia.org/wiki/Endianness)
*
* The method throws if the given bits do not fit in a single Field element. A Field element can be at most 255 bits.
* The method throws if the given bits do not fit in a single Field element. A Field element can be at most 254 bits.
*
* **Important**: If the given `bytes` array is an array of `booleans` or {@link Bool} elements that all are `constant`, the resulting {@link Field} element will be a constant as well. Or else, if the given array is a mixture of constants and variables of {@link Bool} type, the resulting {@link Field} will be a variable as well.
*
Expand All @@ -944,20 +945,14 @@ class Field {
* @return A {@link Field} element matching the [little endian binary representation](https://en.wikipedia.org/wiki/Endianness) of the given `bytes` array.
*/
static fromBits(bits: (Bool | boolean)[]) {
let length = bits.length;
checkBitLength('Field.fromBits()', length);
if (bits.every((b) => typeof b === 'boolean' || b.toField().isConstant())) {
let bits_ = bits
.map((b) => (typeof b === 'boolean' ? b : b.toBoolean()))
.concat(Array(Fp.sizeInBits - length).fill(false));
return new Field(Fp.fromBits(bits_));
}
let bitsVars = bits.map((b): FieldVar => {
if (typeof b === 'boolean') return b ? FieldVar[1] : FieldVar[0];
return b.toField().value;
});
let x = Snarky.field.fromBits([0, ...bitsVars]);
return new Field(x);
const length = bits.length;
checkBitLength('Field.fromBits()', length, 254);
return bits
.map((b) => new Bool(b))
.reduce((acc, bit, idx) => {
const shift = 1n << BigInt(idx);
return acc.add(bit.toField().mul(shift));
}, Field.from(0));
}
jackryanservia marked this conversation as resolved.
Show resolved Hide resolved

/**
Expand Down
Loading