-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
fix: eliminate bn.js and replace public key storage with JavaScript bigint
#27532
Conversation
Codecov Report
@@ Coverage Diff @@
## master solana-labs/solana#27532 +/- ##
=========================================
- Coverage 77.1% 76.2% -0.9%
=========================================
Files 55 54 -1
Lines 2934 3129 +195
Branches 408 474 +66
=========================================
+ Hits 2264 2387 +123
- Misses 529 574 +45
- Partials 141 168 +27 |
I'm not 100% sure this is a slam dunk. I'm looking for feedback.
Although I guess what I said about our patch being isolated from the rest of the app implies that they would keep working, since they use their own mainline version of |
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 great for the most part, mainly a question on the internal representation
'bigint-buffer', | ||
'bn.js', | ||
'borsh', |
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 these be added back once your change lands in borsh? If so, do you mind adding a GitHub issue or comment to that effect?
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.
That's right! I will.
this._bn = value._bn; | ||
if (typeof value._bn === 'object') { | ||
// Legacy implementation of public key storage as a `BN` instance. | ||
this._bn = toBigIntLE(value._bn.toBuffer('le', 32)); |
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 is a breaking change on the internal implementation, because before bn
would default to big-endian: https://github.com/indutny/bn.js/blob/5df40f81ea8afb835b909bb7c21e0833cdeb6a30/lib/bn.js#L39, which caused me a lot of annoyance.
This might be ok since people shouldn't depend on the internal representation, but for this case, if they do provide a legacy bn, maybe we might want to treat the input as big-endian, and then convert it to little-endian here. What do you think?
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 this is OK, actually.
- Someone supplies a
BN
instance to the constructor (endianness is irrelevant at that point) - We convert it to an LE buffer
- We convert that LE buffer to a
bigint
usingtoBigIntLE
(which implements ‘convert this LE buffer to abigint
’)
Now, the internal representation is a bigint
, which doesn't have an endianness – it just exists.
The way in which this is a breaking change is for anyone who ever reaches into PublicKey::_bn
. We never marked that TypeScript private
so there has never been and still is no compile time protection against someone doing so. If we ship this, they will be surprised to find that _bn
is no longer a BN
but a bigint
, and their code will break.
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 duh, of course, you're right, sorry 🤦 I got confused with using borsh encoding that just treated the underlying as little-endian always, which isn't an issue here because you're doing toBuffer('le'
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.
If we ship this, they will be surprised to find that
_bn
is no longer aBN
but abigint
What if we change the name and get rid of the internal _bn
property, or throw when it's accessed with a getter? This will cause access of internals to fail faster, rather than experience a larger range of errors where they get the value and pass it to something else or try to call a method on it that doesn't exist.
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, we could do that. We'll have to add something to the constructor which allows PublicKey
to be part of the PublicKeyInitData
union, because people have gotten used to doing this:
// Like, I've actually seen this.
new PublicKey(new PublicKey(...));
Right now that works because PublicKey
just happens to conform to {_bn: BN}
because, damnit, _bn
was never marked private
.
BigInt( | ||
'115792089237316195423570985008687907853269984665640564039457584007913129639936', | ||
) |
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.
nit: how about making a const for this, ie. PUBLIC_KEY_MAXIMUM_VALUE
and making it this number - 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.
I generally avoid constants, because they make people have to hop around the code. I avoid them doubly when there's literally only one place they're needed.
wrt the - 1
idea, I also generally don't want to make the runtime do math when it doesn't have to.
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.
Sorry, I meant to define it right there, so no hopping, but it's no big deal. And for -1, I meant encoding the string as 11579...9935
instead, so it would really mean PUBLIC_KEY_MAXIMUM_VALUE
This PR breaks |
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 hope you don't mind the force-push to rebase, but otherwise it's just the little change that I've outlined, no rush at all on this.
if (typeof publicKey._bn === 'bigint') { | ||
return this._bn === publicKey._bn; | ||
} else { | ||
// If it's not a bigint, the other type comes from an old library, so we | ||
// just check the underlying buffer representation. | ||
return this.toBuffer().equals(publicKey.toBuffer()); | ||
} |
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 fixes the issue with the tests! Since we were getting an old PublicKey
from spl-token
, this needs to be more permissive with the checks. Uint8Array
doesn't have an equality check, so this seemed like the safest way to check equality between BN
and bigint
, but could also future proof us in case we change the underlying representation again
Thanks for the fix @joncinque! I'd love to smash this in. Do we need to do a little bit of a think on this, given that |
Migrated to solana-labs/solana-web3.js#1142. |
Problem
JavaScript
bigint
has been available in browsers since September 2020. Despite that we've implemented large value storage inPublicKey
using thebn.js
library. This library presently makes up ~13% of@solana/web3.js
, or around 35K uncompressed.Replacing this system with pure
bigint
storage would cut the size of this library down considerably.Summary of Changes
borsh
to serialize to and frombigint
rather thanBN
instances. A pull request has been sent to the upstream project.BN
storage inPublicKey
withbigint
Bundle size
Using
package-build-stats
:Size before: 75299 gzipped bytes
Size after: 64191 gzipped bytes
Reduction of ~14%.
Notes
PublicKey::_bn
will find that its type has changed fromBN
tobigint
. Such is the risk when you reach into private variables.borsh-js
in a way that represents a breaking change. For this reason we modify the bundle script to bundle the modified version into theweb3.js
bundle. This has two implications:borsh
outside ofweb3.js
will end up bundling a second copy of it (presumably one that includes 'bn.js` which negates the benefits of this change)web3.js
and will not cause a breaking change to the rest of the app.Addresses solana-labs/solana-web3.js#1103.