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

Ecrecover point at infinity #76

Merged
merged 1 commit into from
Dec 9, 2021

Conversation

kclowes
Copy link
Contributor

@kclowes kclowes commented Oct 1, 2021

What was wrong?

ecdsa_recover returned a public address even if the Jacobian transformation returned an invalid point (the point at infinity). This behavior was inconsistent with other clients. See: ethereum/tests#941

Once this is merged and released, we'll need to update eth-account and py-evm.

How was it fixed?

Replaced points in the Jacobian files that were meant to represent the points at infinity with the more commonly used representations. And now the ecdsa_recovery method checks to see if Q is a point at infinity and raises an error to be more consistent with Geth's ecrecover.

I went back and forth deciding whether to tack on an is_infinity flag to the point at infinity representation, but ultimately decided to go with the simple solution for now. I'm open to adding an is_infinity flag though if anyone else feels strongly.

Cute Animal Picture

Cute animal picture

@kclowes kclowes force-pushed the ecrecover-point-at-infinity branch from 5bb6cdf to 56dbd42 Compare October 4, 2021 22:15
@kclowes kclowes changed the title [WIP] Ecrecover point at infinity Ecrecover point at infinity Oct 6, 2021
@kclowes
Copy link
Contributor Author

kclowes commented Oct 6, 2021

👋 @ChihChengLiang! I tagged you because Piper said you know the math behind this really well. If you could please take a look to make sure these changes make sense, that would be great!

@kclowes kclowes force-pushed the ecrecover-point-at-infinity branch from 56dbd42 to 86e07d0 Compare October 6, 2021 20:38
Copy link
Collaborator

@carver carver left a comment

Choose a reason for hiding this comment

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

Yeah, I definitely don't understand what's going on here well enough to give a 👍🏻 . The thing that stood out the most to me was that this only seems to fix the problem if we are using the native backend, but we should definitely make sure this fixes the issue for all backends.

tests/backends/test_native_backend_against_coincurve.py Outdated Show resolved Hide resolved
eth_keys/constants.py Outdated Show resolved Hide resolved
tests/backends/test_native_backend_against_coincurve.py Outdated Show resolved Hide resolved
Comment on lines 105 to 107
# This test does introduce some flakiness on the small chance
# that hypothesis generates another message hash that corresponds to the
# Jacobian point at infinity
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm, that seems important. I wonder if we can track down another message hash that also corresponds to the Jacobian point at infinity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, probably a good idea to track down another one for some more testing. I'll see if I can find one!

eth_keys/backends/native/ecdsa.py Outdated Show resolved Hide resolved
@kevaundray
Copy link

kevaundray commented Oct 7, 2021

I think two bugs are introduced with this PR, below I put the rationale.

TLDR

  • The distinguished point was changed to be (1,1,0), but the code is not properly checking for this point anymore when doing Jacobean arithmetic.
  • The new distinguished point is now the point at infinity which has many representations and so checking for just one of them would not be enough. All representations are (t^2, t^3, 0) where t != 0

(Note: I use the term point at infinity, quite loosely below and it should instead be distinguished point since before this PR, those points were not the points at infinity)

Assumptions

  1. The points at infinity, act as the identity element in the group. This means that given a point P, P + POINT_AT_INFINITY = P

  2. Previously, the code checked for the point at infinity by checking the second component of the Jacobian Point to be 0

Evidence for second assumption

See:

If you check the jacobian_add method (2nd link), where we are adding two points p and q , the code checks the second component of p using if not p[1] and on this condition, it returns q. According to the first assumption I made, this is only true, if p is the point identity element. So that if statement is a way of checking for the point at infinity / identity element.

  • If we also check jacobian_multipy method (3rd link), where we are doing a scalar multiplication of the point a with the scalar n , the code first checks the second component of a using if a[1] == 0 , if this is the case then it returns the identity point. This code is essentially saying, if the point is the identity point/ point at infinity, just return it because doing a scalar multiplication with the identity point/point at infinity always returns the identity point.

So both if statements check the second component of the jacobian point, and if it is zero, it says "this is the point at infinity/identity point" judging by what it does on those conditions.

Pre PR

Before the PR was merged, since the points at infinity were represented using either (0,0,0) or (0,0,1), the check above would work.

Note this check implicitly assumes that any point whose second component is 0, is the identity point, I haven't checked any further into assumption, I just assumed it held true.

Bug

If you agree with the above rationale, then I think the first bug was introduced because you now represent the point at infinity using the constant (1,1,0) .

See here: https://github.com/kclowes/eth-keys/blob/ecrecover-point-at-infinity/eth_keys/constants.py#L20

But the logic to check for the point at infinity is still to check for the second component, which is not zero using that constant.

The second bug is because using the representation that you chose, the points at infinity is now a set of points (t^2, t^3, 0) where t !=0. This is in the stack overflow post accompanied with this PR above. This means that the if point == POINT_AT_INFINITY check would not be enough, since it is only checking for one particular representation of the point at infinity, when t = 1, but AFAICT there are no guarantees that this will be the representation that you will get after doing a sequence of point arithmetics.

Testing for bug

  • A test which should fail the CI is to add the point at infinity to itself multiple times, then check that the result is the point at infinity.

Something like the below code

result = POINT_AT_INFINITY
for i in range(0,100):
 result = jacobian_add(result, POINT_AT_INFINITY)
assert result == POINT_AT_INFINITY
  • Another test you can do is to add the point at infinity to a point P and check that the result is P , where P is not the point at infinity.

If I understand your changes, then the two tests should fail.

Suggestion

The code was failing because the ecdsa recover was not checking for the points at infinity IIUC. If we assume that the library is correct in just checking for the second component, then you would only need to check this too in the ecdsa_recover method. You could also explicitly check for the two internal representations being used.

I'd probably add a method called is_identity(p: Tuple[int, int, int]) -> bool that conveys the fact that you are checking for the identity point and abstracts away the implementation details.

@ChihChengLiang
Copy link

@kclowes Sorry I didn't finish my review in time. I think @kevaundray summarized the issue thoroughly.

@kclowes kclowes force-pushed the ecrecover-point-at-infinity branch 2 times, most recently from 42e8d2e to e51a520 Compare October 14, 2021 21:44
@kclowes
Copy link
Contributor Author

kclowes commented Oct 14, 2021

Thanks for the review @kevaundray! I think I fixed the bugs you pointed out, but would appreciate if you could take another look.

If we assume that the library is correct in just checking for the second component, then you would only need to check this too in the ecdsa_recover method. You could also explicitly check for the two internal representations being used.

Is there a way that seems more correct? If the y-coordinate of point Q from here is 0, does that always correspond to an identity point?

@carver - would appreciate another look as well when you have a few!

Copy link
Collaborator

@carver carver left a comment

Choose a reason for hiding this comment

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

Cool, I'm mostly piggy-backing on the expertise of other folks here when I say :shipit:

@kclowes kclowes merged commit 0831844 into ethereum:master Dec 9, 2021
@kclowes kclowes deleted the ecrecover-point-at-infinity branch December 9, 2021 17:07
kclowes added a commit to kclowes/eth-keys that referenced this pull request Jan 21, 2022
pacrob added a commit to pacrob/eth-keys that referenced this pull request Dec 20, 2023
* remove gitter, testing setup, and pandoc sections, add quotes to dev install
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.

5 participants