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
Show file tree
Hide file tree
Changes from 15 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
54 changes: 32 additions & 22 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,43 @@ 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);
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.

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));
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));
}
let [, ...bits] = Snarky.field.toBits(length ?? Fp.sizeInBits, this.value);
return bits.map((b) => new Bool(b));
let bits = Provable.witness(Provable.Array(Bool, length), () => {
let f = this.toBigInt();
return Array.from(
{ length },
(_, k) => new Bool(!!((f >> BigInt(k)) & 0x1n))
);
});
Field.fromBits(bits).assertEquals(
this,
`Field.toBits(): ${Provable.asProver(() => {
this.toString();
jackryanservia marked this conversation as resolved.
Show resolved Hide resolved
})} does not fit in ${length} bits`
);
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. In this case, no more than 254 bits are allowed because some 255 bit integers do not fit into a single Field element.
*
* **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 +954,20 @@ 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);
const length = bits.length;
checkBitLength('Field.fromBits()', length, 254);
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);
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
4 changes: 3 additions & 1 deletion src/lib/field.unit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,9 @@ test(Random.field, Random.field, (x0, y0, assert) => {
Provable.asProver(() => assert(z.toBigInt() === Fp.mul(x0, y0)));

// toBits / fromBits
let bits = Fp.toBits(x0);
// Fp.toBits() returns 255 bits, but our new to/from impl only accepts <=254
// https://github.com/o1-labs/o1js/pull/1461
let bits = Fp.toBits(x0).slice(0, -1);
let x1 = Provable.witness(Field, () => Field.fromBits(bits));
let bitsVars = x1.toBits();
Provable.asProver(() =>
Expand Down
7 changes: 4 additions & 3 deletions src/lib/provable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* - a namespace with tools for writing provable code
* - the main interface for types that can be used in provable code
*/
import { Field, Bool } from './core.js';
import { Bool } from './bool.js';
import { Field } from './field.js';
import { Provable as Provable_, Snarky } from '../snarky.js';
import type { FlexibleProvable, ProvableExtended } from './circuit-value.js';
import { Context } from './global-context.js';
Expand Down Expand Up @@ -239,7 +240,7 @@ function witness<T, S extends FlexibleProvable<T> = FlexibleProvable<T>>(
// }
return [0, ...fieldConstants];
});
fields = fieldVars.map(Field);
fields = fieldVars.map((x) => new Field(x));
} finally {
snarkContext.leave(id);
}
Expand Down Expand Up @@ -388,7 +389,7 @@ function switch_<T, A extends FlexibleProvable<T>>(
if (mask.every((b) => b.toField().isConstant())) checkMask();
else Provable.asProver(checkMask);
let size = type.sizeInFields();
let fields = Array(size).fill(Field(0));
let fields = Array(size).fill(new Field(0));
for (let i = 0; i < nValues; i++) {
let valueFields = type.toFields(values[i]);
let maskField = mask[i].toField();
Expand Down
5 changes: 4 additions & 1 deletion src/lib/scalar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ class Scalar {
* **Warning**: The bits are interpreted as the bits of 2s + 1 + 2^255, where s is the Scalar.
*/
static fromBits(bits: Bool[]) {
return Scalar.fromFields(bits.map((b) => b.toField()));
return Scalar.fromFields([
...bits.map((b) => b.toField()),
...Array(Fq.sizeInBits - bits.length).fill(new Bool(false)),
]);
}

/**
Expand Down
10 changes: 5 additions & 5 deletions tests/vk-regression/vk-regression.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@
}
},
"HelloWorld": {
"digest": "11c21044c0ab297f29b386d1d04d0318cff4effa75191371dd576282882c2a8d",
"digest": "103e8bd9af45de70a1b214377e71bd05ae464e932dbe48aeada70362fa38b05",
"methods": {
"update": {
"rows": 826,
"digest": "5215ab9c7474b26f3a9073d821aef59b"
"rows": 825,
"digest": "226ff56f432f39d8056bf7f1813669f5"
Comment on lines +56 to +57
Copy link
Collaborator

Choose a reason for hiding this comment

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

1-row reduction makes sense because the hello-world example does privateKey.toPublicKey() which as the final step converts the Group (uncompressed EC point (x,y)) into a PublicKey (compressed EC point (x, isOdd)) and to do that it needs to get all of y's bits with toBits(). toBits() needs 1 row less now because it adds up 254 bits, not 255.

It does mean that certain public keys are no longer supported by this method. I think we should address that separately by introducing a dedicated parity() gadget using range checks, which makes it more efficient as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had wondered about this. Would it make sense to have this use toFullBits() here in the meantime?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes that would make sense

}
},
"verificationKey": {
"data": "AAAxHIvaXF+vRj2/+pyAfE6U29d1K5GmGbhiKR9lTC6LJ2o1ygGxXERl1oQh6DBxf/hDUD0HOeg/JajCp3V6b5wytil2mfx8v2DB5RuNQ7VxJWkha0TSnJJsOl0FxhjldBbOY3tUZzZxHpPhHOKHz/ZAXRYFIsf2x+7boXC0iPurETHN7j5IevHIgf2fSW8WgHZYn83hpVI33LBdN1pIbUc7oWAUQVmmgp04jRqTCYK1oNg+Y9DeIuT4EVbp/yN7eS7Ay8ahic2sSAZvtn08MdRyk/jm2cLlJbeAAad6Xyz/H9l7JrkbVwDMMPxvHVHs27tNoJCzIlrRzB7pg3ju9aQOu4h3thDr+WSgFQWKvcRPeL7f3TFjIr8WZ2457RgMcTwXwORKbqJCcyKVNOE+FlNwVkOKER+WIpC0OlgGuayPFwQQkbb91jaRlJvahfwkbF2+AJmDnavmNpop9T+/Xak1adXIrsRPeOjC+qIKxIbGimoMOoYzYlevKA80LnJ7HC0IxR+yNLvoSYxDDPNRD+OCCxk5lM2h8IDUiCNWH4FZNJ+doiigKjyZlu/xZ7jHcX7qibu/32KFTX85DPSkQM8dAGJWqN70atf4Xx5+1igJJNttL6/Dud68IVC2UXBUUI4DdfGREwNEfNTQH87trcq1quxCUnl6kX16UJyWEOvtfC0KR89XcqLS/NP7lwCEej/L8q8R7sKGMCXmgFYluWH4JBSPDgvMxScfjFS33oBNb7po8cLnAORzohXoYTSgztklD0mKn6EegLbkLtwwr9ObsLz3m7fp/3wkNWFRkY5xzSZN1VybbQbmpyQNCpxd/kdDsvlszqlowkyC8HnKbhnvE0Mrz3ZIk4vSs/UGBSXAoESFCFCPcTq11TCOhE5rumMJErv5LusDHJgrBtQUMibLU9A1YbF7SPDAR2QZd0yx3wZuQAviIfujc7i53KrM3hMFmAGPhh/nWhLbDWe/E7wfKEjKaKpMhbGeZnIPPxOP4vz0cCLpsDspPpqpOTuyuRMm8eQmivAYw4xzQi9npkMTvOw+xpZZaj920XMfmz2lyCtVmpb2d8SEG6iBv7/+uucSLr/EI1bDE2xgv3wffc1aMLn9RIlNIt7vJmh7Iur+6aa6xvkXZoRRfn7Y5KYspzAXT0HxnCnt7wnGkUgeiGukBEfuQHg2kSRfhFG3YJy+tiAxOGUbSHzawovjubcH7qWjIZoghZJ16QB1c0ryiAfHB48OHhs2p/JZWz8Dp7kfcPkeg2Of2NbupJlNVMLIH4IGWaPAscBRkZ+F4oLqOhJ5as7fAzzU8PQdeZi0YgssGDJVmNEHP61I16KZNcxQqR0EUVwhyMmYmpVjvtfhHi/6I8WMJpDOHSQwcAmuN1EvZXRsqSyp0pvU681UsdTc480gz//qHhFaiG+fFs0Hgg6xW6npKpBIMH+w/0P0Bqlb5Q5VmlVsP8zA+xuHylyiww/Lercce7cq0YA5PtYS3ge9IDYwXckBUXb5ikD3alrrv5mvMu6itB7ix2f8lbiF9Fkmc4Bk2ycIWXJDCuBN+2sTFqzUeoT6xY8XWaOcnDvqOgSm/CCSv38umiOE2jEpsKYxhRc6W70UJkrzd3hr2DiSF1I2B+krpUVK1GeOdCLC5sl7YPzk+pF8183uI9wse6UTlqIiroKqsggzLBy/IjAfxS0BxFy5zywXqp+NogFkoTEJmR5MaqOkPfap+OsD1lGScY6+X4WW/HqCWrmA3ZTqDGngQMTGXLCtl6IS/cQpihS1NRbNqOtKTaCB9COQu0oz6RivBlywuaj3MKUdmbQ2gVDj+SGQItCNaXawyPSBjB9VT+68SoJVySQsYPCuEZCb0V/40n/a7RAbyrnNjP+2HwD7p27Pl1RSzqq35xiPdnycD1UeEPLpx/ON65mYCkn+KLQZmkqPio+vA2KmJngWTx+ol4rVFimGm76VT0xCFDsu2K0YX0yoLNH4u2XfmT9NR8gGfkVRCnnNjlbgHQmEwC75+GmEJ5DjD3d+s6IXTQ60MHvxbTHHlnfmPbgKn2SAI0uVoewKC9GyK6dSaboLw3C48jl0E2kyc+7umhCk3kEeWmt//GSjRNhoq+B+mynXiOtgFs/Am2v1TBjSb+6tcijsf5tFJmeGxlCjJnTdNWBkSHpMoo6OFkkpA6/FBAUHLSM7Yv8oYyd0GtwF5cCwQ6aRTbl9oG/mUn5Q92OnDMQcUjpgEho0Dcp2OqZyyxqQSPrbIIZZQrS2HkxBgjcfcSTuSHo7ONqlRjLUpO5yS95VLGXBLLHuCiIMGT+DW6DoJRtRIS+JieVWBoX0YsWgYInXrVlWUv6gDng5AyVFkUIFwZk7/3mVAgvXO83ArVKA4S747jT60w5bgV4Jy55slDM=",
"hash": "12073693408844675717690269959590209616934536940765116494766700428390466839"
"data": "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",
"hash": "21868179330353774930881761197110482602042218337984430556708224781269131363222"
}
},
"TokenContract": {
Expand Down
Loading