-
Notifications
You must be signed in to change notification settings - Fork 104
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
Foreign fields 1: Multi-range-check gadgets #1216
Changes from all commits
e9557c5
3f93ac6
4491f7c
00814a3
ca1cf27
c2a2e0a
f11d393
302add7
96c4797
1a666e8
9a594c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,10 +2,10 @@ import { Field } from '../field.js'; | |
import * as Gates from '../gates.js'; | ||
import { bitSlice, exists } from './common.js'; | ||
|
||
export { rangeCheck64 }; | ||
export { rangeCheck64, multiRangeCheck, compactMultiRangeCheck, L }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
/** | ||
* Asserts that x is in the range [0, 2^64), handles constant case | ||
* Asserts that x is in the range [0, 2^64) | ||
*/ | ||
function rangeCheck64(x: Field) { | ||
if (x.isConstant()) { | ||
|
@@ -48,3 +48,154 @@ function rangeCheck64(x: Field) { | |
false // not using compact mode | ||
); | ||
} | ||
|
||
// default bigint limb size | ||
const L = 88n; | ||
const twoL = 2n * L; | ||
const lMask = (1n << L) - 1n; | ||
|
||
/** | ||
* Asserts that x, y, z \in [0, 2^88) | ||
*/ | ||
function multiRangeCheck([x, y, z]: [Field, Field, Field]) { | ||
if (x.isConstant() && y.isConstant() && z.isConstant()) { | ||
if (x.toBigInt() >> L || y.toBigInt() >> L || z.toBigInt() >> L) { | ||
throw Error(`Expected fields to fit in ${L} bits, got ${x}, ${y}, ${z}`); | ||
} | ||
return; | ||
} | ||
|
||
let [x64, x76] = rangeCheck0Helper(x); | ||
let [y64, y76] = rangeCheck0Helper(y); | ||
rangeCheck1Helper({ x64, x76, y64, y76, z, yz: new Field(0) }); | ||
} | ||
|
||
/** | ||
* Compact multi-range-check - checks | ||
* - xy = x + 2^88*y | ||
* - x, y, z \in [0, 2^88) | ||
* | ||
* Returns the full limbs x, y, z | ||
*/ | ||
function compactMultiRangeCheck(xy: Field, z: Field): [Field, Field, Field] { | ||
mitschabaude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// constant case | ||
if (xy.isConstant() && z.isConstant()) { | ||
if (xy.toBigInt() >> twoL || z.toBigInt() >> L) { | ||
throw Error( | ||
`Expected fields to fit in ${twoL} and ${L} bits respectively, got ${xy}, ${z}` | ||
); | ||
} | ||
let [x, y] = splitCompactLimb(xy.toBigInt()); | ||
return [new Field(x), new Field(y), z]; | ||
} | ||
|
||
let [x, y] = exists(2, () => splitCompactLimb(xy.toBigInt())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a question for me here :D There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
My thinking was that for low-level gadgets, everything will be arrays of field elements so it makes sense to have something which optimizes for that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awesome! Thank you for the explanation. I must admit, I used There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice! |
||
|
||
let [z64, z76] = rangeCheck0Helper(z, false); | ||
let [x64, x76] = rangeCheck0Helper(x, true); | ||
rangeCheck1Helper({ x64: z64, x76: z76, y64: x64, y76: x76, z: y, yz: xy }); | ||
|
||
return [x, y, z]; | ||
} | ||
|
||
function splitCompactLimb(x01: bigint): [bigint, bigint] { | ||
return [x01 & lMask, x01 >> L]; | ||
} | ||
|
||
function rangeCheck0Helper(x: Field, isCompact = false): [Field, Field] { | ||
// crumbs (2-bit limbs) | ||
let [x0, x2, x4, x6, x8, x10, x12, x14] = exists(8, () => { | ||
let xx = x.toBigInt(); | ||
return [ | ||
bitSlice(xx, 0, 2), | ||
bitSlice(xx, 2, 2), | ||
bitSlice(xx, 4, 2), | ||
bitSlice(xx, 6, 2), | ||
bitSlice(xx, 8, 2), | ||
bitSlice(xx, 10, 2), | ||
bitSlice(xx, 12, 2), | ||
bitSlice(xx, 14, 2), | ||
]; | ||
}); | ||
|
||
// 12-bit limbs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems different than the rust implementation. In the rust implementation the crumbs are in columns 7-14. Maybe it has to do with the endianness... in the rust implementation of the gate limbs are mapped to columns in big-endian order (the lowest columns contain the highest bits). The reason for this is so that the hiehgest bits are covered by copy constraints and lookups, so that we can copy the highest two 12-bit limbs to zero in order to use the You probably have this right here and it's just because of a different byte-order, but I just wanted to call it out explicitly to make sure. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so, I guess I create the witnesses in little endian order here (which seemed more natural to me), but note that I pass them to the gate in big endian order, see here |
||
let [x16, x28, x40, x52, x64, x76] = exists(6, () => { | ||
let xx = x.toBigInt(); | ||
return [ | ||
bitSlice(xx, 16, 12), | ||
bitSlice(xx, 28, 12), | ||
bitSlice(xx, 40, 12), | ||
bitSlice(xx, 52, 12), | ||
bitSlice(xx, 64, 12), | ||
bitSlice(xx, 76, 12), | ||
]; | ||
}); | ||
|
||
Gates.rangeCheck0( | ||
x, | ||
[x76, x64, x52, x40, x28, x16], | ||
[x14, x12, x10, x8, x6, x4, x2, x0], | ||
isCompact | ||
); | ||
|
||
// the two highest 12-bit limbs are returned because another gate | ||
// is needed to add lookups for them | ||
return [x64, x76]; | ||
} | ||
|
||
function rangeCheck1Helper(inputs: { | ||
x64: Field; | ||
x76: Field; | ||
y64: Field; | ||
y76: Field; | ||
z: Field; | ||
yz: Field; | ||
}) { | ||
let { x64, x76, y64, y76, z, yz } = inputs; | ||
|
||
// create limbs for current row | ||
let [z22, z24, z26, z28, z30, z32, z34, z36, z38, z50, z62, z74, z86] = | ||
exists(13, () => { | ||
let zz = z.toBigInt(); | ||
return [ | ||
bitSlice(zz, 22, 2), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which endianness is this in? The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is little endian - |
||
bitSlice(zz, 24, 2), | ||
bitSlice(zz, 26, 2), | ||
bitSlice(zz, 28, 2), | ||
bitSlice(zz, 30, 2), | ||
bitSlice(zz, 32, 2), | ||
bitSlice(zz, 34, 2), | ||
bitSlice(zz, 36, 2), | ||
bitSlice(zz, 38, 12), | ||
bitSlice(zz, 50, 12), | ||
bitSlice(zz, 62, 12), | ||
bitSlice(zz, 74, 12), | ||
bitSlice(zz, 86, 2), | ||
]; | ||
}); | ||
|
||
// create limbs for next row | ||
let [z0, z2, z4, z6, z8, z10, z12, z14, z16, z18, z20] = exists(11, () => { | ||
let zz = z.toBigInt(); | ||
return [ | ||
bitSlice(zz, 0, 2), | ||
bitSlice(zz, 2, 2), | ||
bitSlice(zz, 4, 2), | ||
bitSlice(zz, 6, 2), | ||
bitSlice(zz, 8, 2), | ||
bitSlice(zz, 10, 2), | ||
bitSlice(zz, 12, 2), | ||
bitSlice(zz, 14, 2), | ||
bitSlice(zz, 16, 2), | ||
bitSlice(zz, 18, 2), | ||
bitSlice(zz, 20, 2), | ||
]; | ||
}); | ||
|
||
Gates.rangeCheck1( | ||
z, | ||
yz, | ||
[z86, z74, z62, z50, z38, z36, z34, z32, z30, z28, z26, z24, z22], | ||
[z20, z18, z16, x76, x64, y76, y64, z14, z12, z10, z8, z6, z4, z2, z0] | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,127 @@ | ||
import type { Gate } from '../../snarky.js'; | ||
import { mod } from '../../bindings/crypto/finite_field.js'; | ||
import { Field } from '../../lib/core.js'; | ||
import { ZkProgram } from '../proof_system.js'; | ||
import { Provable } from '../provable.js'; | ||
import { | ||
Spec, | ||
boolean, | ||
equivalentAsync, | ||
field, | ||
fieldWithRng, | ||
} from '../testing/equivalent.js'; | ||
import { Random } from '../testing/property.js'; | ||
import { assert, exists } from './common.js'; | ||
import { Gadgets } from './gadgets.js'; | ||
import { L } from './range-check.js'; | ||
import { expect } from 'expect'; | ||
|
||
let maybeUint64: Spec<bigint, Field> = { | ||
...field, | ||
rng: Random.map(Random.oneOf(Random.uint64, Random.uint64.invalid), (x) => | ||
mod(x, Field.ORDER) | ||
), | ||
let uint = (n: number | bigint): Spec<bigint, Field> => { | ||
let uint = Random.bignat((1n << BigInt(n)) - 1n); | ||
return fieldWithRng(uint); | ||
}; | ||
|
||
let maybeUint = (n: number | bigint): Spec<bigint, Field> => { | ||
let uint = Random.bignat((1n << BigInt(n)) - 1n); | ||
return fieldWithRng( | ||
Random.map(Random.oneOf(uint, uint.invalid), (x) => mod(x, Field.ORDER)) | ||
); | ||
}; | ||
|
||
// constraint system sanity check | ||
|
||
function csWithoutGenerics(gates: Gate[]) { | ||
return gates.map((g) => g.type).filter((type) => type !== 'Generic'); | ||
} | ||
|
||
let check64 = Provable.constraintSystem(() => { | ||
let [x] = exists(1, () => [0n]); | ||
Gadgets.rangeCheck64(x); | ||
}); | ||
let multi = Provable.constraintSystem(() => { | ||
let x = exists(3, () => [0n, 0n, 0n]); | ||
Gadgets.multiRangeCheck(x); | ||
}); | ||
let compact = Provable.constraintSystem(() => { | ||
let [xy, z] = exists(2, () => [0n, 0n]); | ||
Gadgets.compactMultiRangeCheck(xy, z); | ||
}); | ||
|
||
let expectedLayout64 = ['RangeCheck0']; | ||
let expectedLayoutMulti = ['RangeCheck0', 'RangeCheck0', 'RangeCheck1', 'Zero']; | ||
|
||
expect(csWithoutGenerics(check64.gates)).toEqual(expectedLayout64); | ||
expect(csWithoutGenerics(multi.gates)).toEqual(expectedLayoutMulti); | ||
expect(csWithoutGenerics(compact.gates)).toEqual(expectedLayoutMulti); | ||
|
||
// TODO: make a ZkFunction or something that doesn't go through Pickles | ||
// -------------------------- | ||
// RangeCheck64 Gate | ||
// -------------------------- | ||
|
||
let RangeCheck64 = ZkProgram({ | ||
name: 'range-check-64', | ||
let RangeCheck = ZkProgram({ | ||
name: 'range-check', | ||
methods: { | ||
run: { | ||
check64: { | ||
privateInputs: [Field], | ||
method(x) { | ||
Gadgets.rangeCheck64(x); | ||
}, | ||
}, | ||
checkMulti: { | ||
privateInputs: [Field, Field, Field], | ||
method(x, y, z) { | ||
Gadgets.multiRangeCheck([x, y, z]); | ||
}, | ||
}, | ||
checkCompact: { | ||
privateInputs: [Field, Field], | ||
method(xy, z) { | ||
let [x, y] = Gadgets.compactMultiRangeCheck(xy, z); | ||
x.add(y.mul(1n << L)).assertEquals(xy); | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
await RangeCheck64.compile(); | ||
await RangeCheck.compile(); | ||
|
||
// TODO: we use this as a test because there's no way to check custom gates quickly :( | ||
await equivalentAsync({ from: [maybeUint64], to: boolean }, { runs: 3 })( | ||
|
||
await equivalentAsync({ from: [maybeUint(64)], to: boolean }, { runs: 3 })( | ||
(x) => { | ||
if (x >= 1n << 64n) throw Error('expected 64 bits'); | ||
assert(x < 1n << 64n); | ||
return true; | ||
}, | ||
async (x) => { | ||
let proof = await RangeCheck64.run(x); | ||
return await RangeCheck64.verify(proof); | ||
let proof = await RangeCheck.check64(x); | ||
return await RangeCheck.verify(proof); | ||
mitschabaude marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
); | ||
|
||
await equivalentAsync( | ||
{ from: [maybeUint(L), uint(L), uint(L)], to: boolean }, | ||
{ runs: 3 } | ||
)( | ||
(x, y, z) => { | ||
assert(!(x >> L) && !(y >> L) && !(z >> L), 'multi: not out of range'); | ||
return true; | ||
}, | ||
async (x, y, z) => { | ||
let proof = await RangeCheck.checkMulti(x, y, z); | ||
return await RangeCheck.verify(proof); | ||
} | ||
); | ||
|
||
await equivalentAsync( | ||
{ from: [maybeUint(2n * L), uint(L)], to: boolean }, | ||
{ runs: 3 } | ||
)( | ||
(xy, z) => { | ||
assert(!(xy >> (2n * L)) && !(z >> L), 'compact: not out of range'); | ||
return true; | ||
}, | ||
async (xy, z) => { | ||
let proof = await RangeCheck.checkCompact(xy, z); | ||
return await RangeCheck.verify(proof); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any out of range tests. might be a good idea. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a test for both in range and out of range! the function signature spec is { from: [maybeUint(2n * L), uint(L)], to: boolean }, in our test framework, this means that the first argument There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
} | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you intending that the compact range-check is usable externally as part of the API or is it only for the internal optimization?
Maybe a good idea to note any limitations of this gadget and when this is safe to use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah - so for context, the way we expose this is on a
Gadgets
namespace which is presented as a set of low-level provable methods for advanced users (including ourselves, in higher-level APIs). So I think the compact range check fits well to include. (It gives you something you can't do in any other way, so in a sense it can't be left out)What do you think are limitations/security concerns that should be highlighted?