-
Notifications
You must be signed in to change notification settings - Fork 97
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
Hints for arithmetic machine #1005
Conversation
62326e4
to
3a0e339
Compare
ac33b86
to
fc52613
Compare
cf20314
to
b9826bd
Compare
b9826bd
to
d6564fe
Compare
d6564fe
to
d38f3d4
Compare
d38f3d4
to
d712628
Compare
d712628
to
bf44677
Compare
0e9df15
to
e9d7805
Compare
4b4ebc3
to
79dc4f3
Compare
std/arith.asm
Outdated
(|x1, y1, x2, s| | ||
(|x3| | ||
compute_q2(x1, y1, x3, compute_y3_int(x1, y1, x3, s), s, limb_index) | ||
)(compute_x3_int(x1, x2, s)) | ||
)(x1_int(i), y1_int(i), x2_int(i), s_int(i)) |
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.
This used to be:
compute_q2(x1_int(i), y1_int(i), compute_x3_int(x1_int(i), x2_int(i), s_int(i)), compute_y3_int(x1_int(i), y1_int(i), compute_x3_int(x1_int(i), x2_int(i), s_int(i)), s_int(i)), s_int(i), limb_index)
This get's rid of duplicate calls to various functions. If we had let statements, the equivalent code would be:
let x1 = x1_int(i);
let y1 = y1_int(i);
let x2 = x2_int(i);
let s = s_int(i);
let x3 = compute_x3_int(x1, x2, s);
let y3 = compute_y3_int(x1, y1, x3, s);
compute_q2(x1, y1, x3, y3,, s, limb_index)
@@ -103,7 +103,7 @@ machine Main{ | |||
assert_eq t_0_0, t_0_1, t_0_2, t_0_3, t_0_4, t_0_5, t_0_6, t_0_7, 0x9be02469, 0xf258bf25, 0x38e38e38, 0xe6f8091a, 0x740da740, 0x579be024, 0x091a2b3c, 0x00000000; | |||
assert_eq t_1_0, t_1_1, t_1_2, t_1_3, t_1_4, t_1_5, t_1_6, t_1_7, 0x33333333, 0xa1907f6e, 0xca8641fd, 0x369d0369, 0x907f6e5d, 0x60b60b60, 0x0da740da, 0x1fdb9753; | |||
|
|||
// Test vectors from: https://github.com/0xPolygonHermez/zkevm-proverjs/blob/main/test/sm/sm_arith.js | |||
// Test vectors from: https://github.com/0xPolygonHermez/zkevm-proverjs/blob/a4006af3d7fe4a57a85500c01dc791fb5013cef0/test/sm/sm_arith.js |
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.
In the meantime, Polygon has a new test file, because they also added additional equation to the arithmetic machine.
assert_eq t_1_0, t_1_1, t_1_2, t_1_3, t_1_4, t_1_5, t_1_6, t_1_7, 0x6a255906, 0xb1976404, 0x3c3747d4, 0xebd4f608, 0x6bd3bbf, 0x40d90c4b, 0x23adb6fd, 0xb5196132; | ||
|
||
// Left out these test cases, because the format is different... | ||
// https://github.com/0xPolygonHermez/zkevm-proverjs/blob/a4006af3d7fe4a57a85500c01dc791fb5013cef0/test/sm/sm_arith.js#L1196-L1211 |
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.
Not sure if these are important; they might have just wanted to test their addPoint()
JS function?
pipeline/tests/powdr_std.rs
Outdated
@@ -44,7 +44,8 @@ fn split_gl_test() { | |||
fn arith_test() { | |||
let f = "std/arith_test.asm"; | |||
verify_test_file::<GoldilocksField>(f, Default::default(), vec![]); | |||
gen_estark_proof(f, Default::default()); | |||
// Otherwise, we get: thread 'arith_test' has overflowed its stack | |||
// gen_estark_proof(f, Default::default()); |
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.
😢 I wonder why this happens. This line works just fine:
cargo run --features halo2 -r pil test_data/std/arith_test.asm -o output -f --prove-with estark
Also, I didn't add any columns or constraints in this PR (except maybe the main machine in arith_test.asm
), so I don't see how it would make a difference for eStark?
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.
Maybe we should investigate this a bit. Is it only a problem for us?
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.
So what's strange is that that's only in the test, I can generate an eStark proof for this in the command line (but maybe it's just below the limit). We also have this in several other tests, so it's not a problem unique to the arithmetic machine.
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.
Looks like this problem just disappeared.
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.
Tests run in threads with limited memory iirc
std/arith.asm
Outdated
let s_for_eq2 = |x1, y1, limb_index| (div(mul(3, mul(x1, x1)), mul(2, y1)) >> (limb_index * 16)) & 0xffff; | ||
|
||
let compute_x3_int = |x1, x2, s| (s * s - x1 - x2 + 2 * secp_modulus) % secp_modulus; | ||
let compute_y3_int = |x1, y1, x3, s| (s * (((x1 - x3) + secp_modulus) % secp_modulus) - y1 + secp_modulus) % secp_modulus; |
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.
Why the intermediate % secp_modulus
?
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.
Also + secp_modulus) % secp_modulus
?
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.
Why the intermediate
% secp_modulus
?
True, I can remove that. I it reduces the size of the big int before doing the addition, but probably not worth it.
Also
+ secp_modulus) % secp_modulus
?
That's needed because %
behaves weird on negative numbers, e.g. -1 % 5 = -1
. Adding the modulus ensures %
is only applied to positive numbers. I'll add a comment.
std/arith.asm
Outdated
let compute_y3_int = |x1, y1, x3, s| (s * (((x1 - x3) + secp_modulus) % secp_modulus) - y1 + secp_modulus) % secp_modulus; | ||
|
||
// Note that the most significant limb is 32-Bit instead of 16 | ||
let compute_q0_for_eq1 = |x1, y1, x2, y2, s, limb_index| ((-(s * x2 - s * x1 - y2 + y1) / secp_modulus + (1 << 258)) >> (limb_index * 16)) & if limb_index < 15 { 0xffff } else { 0xffffffff }; |
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.
Can you split those up a bit? I cannot really parse it.
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, good call! I made a select_limb(x, limb_index)
function, and actually pulled it out all the way to the actual hint. This has the extra benefit that the evaluation cache will be much more efficient, because e.g. compute_q0_for_eq1
won't have a limb_index
argument anymore!
std/arith.asm
Outdated
let compute_q1 = |x1, x2, x3, s, limb_index| ((-(s * s - x1 - x2 - x3) / secp_modulus + (1 << 258)) >> (limb_index * 16)) & if limb_index < 15 { 0xffff } else { 0xffffffff }; | ||
let compute_q2 = |x1, y1, x3, y3, s, limb_index| ((-(s * x1 - s * x3 - y1 - y3) / secp_modulus + (1 << 258)) >> (limb_index * 16)) & if limb_index < 15 { 0xffff } else { 0xffffffff }; | ||
|
||
let limbs_to_int = |limbs, row| array::sum(array::new(array::len(limbs), |i| int(limbs[i](row)) << (i * 16))); |
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.
We could add a map_enumerated
function to array.
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.
👍
std/arith.asm
Outdated
let x3_int: int -> int = |row| limbs_to_int(x3, row); | ||
let s_int: int -> int = |row| limbs_to_int(s, row); | ||
|
||
let s_hint = |i, limb_index| if selEq[1](i) == fe(1) { |
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.
Ok, here we really need to evaluate a witness column. So for this I need to add a builtin eval
function, but I think this should be doable.
9059347
to
5d0826f
Compare
std/arith.asm
Outdated
|
||
let limbs_to_int: expr[] -> int = |limbs| array::sum(array::map_enumerated(limbs, |i, limb| int(eval(limb)) << (i * 16))); | ||
|
||
let x1_int: -> int = || limbs_to_int(x1); |
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.
Ok this is a bit weird because in a pure functional sense, this should be a constant. Maybe we can at some point also mark functions that need to be evaluated in a query context.
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.
What is also not very clear to me is that the way this ends up being used is:
col witness s_0(i) query ("hint", fe(select_limb(s_hint(), 0)));
So the call to s_hint()
is evaluated at witgen time (because otherwise it would hit eval()
, which would fail?). Maybe it would be good to make that explicit somehow, e.g.:
col witness s_0: witness = |i| ("hint", fe(select_limb(s_hint(), 0)));
Then witness and fixed columns would be specified the same, but depending on the type the callback gets executed in different phases? I think that would also give us arrays of witness columns with hints, just like we already have arrays of fixed column?
std/arith.asm
Outdated
let x3_int: -> int = || limbs_to_int(x3); | ||
let s_int: -> int = || limbs_to_int(s); | ||
|
||
let s_hint: -> int = || if eval(selEq[1]) == 1 { |
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.
maybe have a helper function eq1_active()
?
std/arith.asm
Outdated
}; | ||
|
||
let q0_hint: -> int = || if eval(selEq[1]) == 1 { | ||
compute_q0_for_eq1(x1_int(), y1_int(), x2_int(), y2_int(), s_int()) |
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.
Does it make sense to pass the x1_int
etc functions if the are always the same? I think this has a too big danger for mixing up the different arguments.
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.
I started doing that but we'll still want to do the (|x1, y1, ...| ...)(x1_int(), y2_int())
trick so we don't have to re-compute the integers if they are used multiple times. So I don't really see an improvement, maybe when we have let statements that changes.
assert_eq t_1_0, t_1_1, t_1_2, t_1_3, t_1_4, t_1_5, t_1_6, t_1_7, 0x762cef4, 0xbbc02738, 0xc062b742, 0xbe040a8, 0x40e28465, 0xf6f29283, 0x68008032, 0x912770e0; | ||
|
||
t_0_0, t_0_1, t_0_2, t_0_3, t_0_4, t_0_5, t_0_6, t_0_7, t_1_0, t_1_1, t_1_2, t_1_3, t_1_4, t_1_5, t_1_6, t_1_7 <== ec_double( | ||
0x33ce1752, 0xc7b750f7, 0xd7cd204e, 0xe783c797, 0xd99c9aea, 0x812ddf64, 0xd01dc635, 0xd7a0da58, |
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.
You can use proper integers now.
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.
What do you mean?
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.
Oh I thought these are split into limbs because the parser cannot parse large numbers, but it's because of the registers.
12 => "ec_double", | ||
_ => panic("Unknown operation") | ||
}; | ||
let is_ec_operation: -> int = || match get_operation() { |
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.
This should return a bool, but I couldn't make it work:
- Returning
true
andfalse
didn't work (can't find the symbol). Are these literals not available? - Returning
get_operation() != "affine_256"
didn't work (string doesn't implement equality).
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.
You can add the boolean constants as let true = (1 == 1)
or something.
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.
Ah probably 1 > 0
, otherwise the type is not uniquely defined.
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.
Hmm, I tried:
let true = 1 >= 1;
let false = 1 > 1;
But this gives me:
Error during type inference:
Error specializing generic references in main_arith.true:
Unable to derive concrete type for literal 1.
Error specializing generic references in main_arith.true:
Unable to derive concrete type for literal 1.
Error specializing generic references in main_arith.false:
Unable to derive concrete type for literal 1.
Error specializing generic references in main_arith.false:
Unable to derive concrete type for literal 1.
1 => "affine_256", | ||
10 => "ec_add", | ||
12 => "ec_double", |
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.
I think soon we'll be able to use enums? :)
std/arith.asm
Outdated
let compute_x3_int = |x1, x2, s| (s * s - x1 - x2 + 2 * secp_modulus) % secp_modulus; | ||
let compute_y3_int = |x1, y1, x3, s| (s * ((x1 - x3) + secp_modulus) - y1 + secp_modulus) % secp_modulus; | ||
|
||
// Compute quotients for the various equation. |
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.
// Compute quotients for the various equation. | |
// Compute quotients for the various equations. |
// Note that the most significant limb can be up to 32 bits; all others are 16 bits. | ||
let select_limb = |x, i| fe((x >> (i * 16)) & if i < 15 { 0xffff } else { 0xffffffff }); | ||
|
||
let s_for_eq1 = |x1, y1, x2, y2| div(sub(y2, y1), sub(x2, x1)); |
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.
Should we not make this
let s_for_eq1 = |x1, y1, x2, y2| div(sub(y2, y1), sub(x2, x1)); | |
let s_for_eq1 = || div(sub(y2_int(), y1_int()), sub(x2_int(), x1_int())); |
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.
Hmm, sure, but for other functions like s_for_eq2
, this would mean computing the same number twice. And doing it only for s_for_eq1
is inconsistent. So I'd leave it like this for now and revisit once we have let
statements.
}; | ||
|
||
let q1_hint: -> int = || if is_ec_operation() == 1 { | ||
// TODO: Make this more readable once we have let statements |
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.
Is this for performance reasons?
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.
Yes! So we don't compute x1
and x2
twice.
std/arith.asm
Outdated
0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff | ||
]); | ||
let p: int -> expr = |i| if i >= 0 && i < 16 { | ||
expr((secp_modulus >> (i * 16)) & 0xffff) |
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.
We could use a byte_slice function here (and also use that for select_limb?)
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.
You mean a function that can select arbitrary bit ranges? Seems a bit overkill, no?
I just tried having a select_limb: int, int -> int
function and using it here. I'm getting a stack overflow! I'll look into why, hopefully it's something we can fix.
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.
I just tried the same thing again and it worked 😮 Maybe I did something wrong the other day.
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.
The main problem I have with this is matching i * 16
with & 0xffff
, but it's fine.
std/arith.asm
Outdated
|
||
let eq1 = (|| |nr| product(sf, x2f)(nr) - product(sf, x1f)(nr) - y2f(nr) + y1f(nr) + product_with_p(q0f)(nr))(); | ||
let eq1: int -> expr = |nr| product(sf, x2f)(nr) - product(sf, x1f)(nr) - y2f(nr) + y1f(nr) + product_with_p(q0f)(nr); |
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.
Do you really need the explicit type?
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.
Looks like I don't! Also many other type declarations.
Adds hints to the arithmetic machine, which fixes witness generation. I also converted Polygon's test cases.