There's Math.random(), and then there's Math.random()
v8project.blogspot.com
v8project.blogspot.com
Also because the specification has very little by way of requirements in that regard, no matter how good some implementations may be you should always assume you code may end up being used in an environment where Math.random() is no better than the worst generator you can think of.
If you need specific properties in your PRNG then you still need to provide something in place of Math.random().
Would you have any suggested reading material on the topic?
It's advocating for a particular new kind of PRNG, but it also includes a lot of great explanations and citations for more reading.
The Art of Computer Programming, Vol. 2, (c) 1969, by Donald Knuth, has quite an extensive discussion of random numbers, their properties, algorithms, and tests. It's about 160 pages in my edition. It's very readable, and even if you skip all the exercises you will still get a very good grounding.
I don't know how much has changed in his more recent editions. And I don't know how if there are better books. But, in general, you can't go wrong by starting with Knuth. He's one of the all time greats!
Yes, since the original post wanted seedability:
> > For games, the most important features (other than the features of an RNG that all applications desire, such as good performance, high period, etc.) are being able to seed the state, and save/restore the state (required for save game support if you need seeding and want that to work after they save and restore the game).
Given that restriction, what's wrong with the generator I suggest?
No, it's a pseudorandom generator. It's a key attribute of a cryptographically secure function like SHA256 that its output is indistinguishable from random bits.
> You are just returning a different representation of the seed you already have.
I'm returning a function of the seed, yes. That's the whole point of a seeded PRNG: that seed(x); rng(); rng(); rng() will always return the same sequence of results. This is used for example in procedural generation, to ensure that a dungeon level or map chunk is always generated the same way (a cool hack, then, is to just store the seed instead of the level itself, and rerun the level-generation algorithm with the seed when one wishes to regenerate the level). In such an application one wants the appearance of randomness, but with repeatability.
Which was what the original post asked for.
What I suggested is not secure for generating keys or for other cryptographic purposes.
If it does, doesn't that mean it's flawed as a cryptographic hash?
Performant enough strongly depends on the game. However looking at the implementation of the algorithm on wikipedia[0], that looks extremely slow by RNG standards. Most RNGs finish in a handful of instructions, but building the 'message schedule array' takes 64 steps, and the 'main compression loop' also takes 64 steps. Consider that to reduce these to a range you'll have to throw out iterations (you need to do this with any RNG to avoid bias), you're looking at a lot of wasted time.
Maybe you can afford to waste that time, but given that there are much simpler and more efficient RNG algorithms, I'm not sure why you wouldn't use one of those.
Of course you need to make sure the initial seed is sufficiently random otherwise you'll risk generating the same sequence each time, which could make this a chicken->egg->chicken situation!
Decrementing the seed by 1 with each call[0], which is a standards-compliant rand implementation.
Although it looks like a standards-compliant Math.random() implementation isn't allowed to be quite that bad[1].
[0] https://marc.info/?l=openbsd-tech&m=141773078029373&w=2
[1] http://www.ecma-international.org/ecma-262/5.1/#sec-15.8.2.1...
So how good does the default random number routine in JavaScript really need to be?
Maybe it's time to have Math.random and equivalents call a CSPRNG, with a Math.insecurerandom when performance matters?
Last time this came up on HN, I measured individual reads from /dev/urandom, and those are fast enough for many large-scale applications, like one per tweet: https://news.ycombinator.com/item?id=10608843
(Obviously there are bad CSPRNGs, but there are also bad non-CS PRNGs. So let's not count those.)
The same problem exists with hash functions.
First, apparently that was unclear and I left it unspecified so note that my mention was intended about general hash functions (as you'd use in a hash table, which could easily bottleneck a benchmark without its developer realising it) not specifically cryptographic ones.
Second, so are number generators and more or less everything else (aside from KDFs), the issue is none of these are only supposed to be fast, and while they should be as fast it's usually more important that they're good, yet the incentive for language developer is to ignore good (whatever that is for the domain) and focus solely on fast. That is what my comment was about.
Basically the idea is that you have a seed which is used as an encryption key, and then you encrypt a counter value to produce an output. Using the next counter value represents the second output from the generator, and so forth. Different seeds produce different output sequences. If you accept that the encryption algorithm works properly, then the output is uncorrelated for a given counter sequence or seed sequence (although seed sequences are slightly less random than counter sequences).
You don't need to store state, and you can seek to an arbitrary position in the PRNG stream in O(1) time simply by recomputing the output for the desired counter value. For non-cryptographic applications, you can derive the seed value from some unique property of your dataset (eg id number of an item being operated upon) and then you don't even need to explicitly store the key value.
On AES-NI compatible hardware the cryptographically strong varients outperform even crushable PRNGs like Mersenne Twister. These are a fantastic choice where you have that acceleration. Your computer is built to do it really fast, and this lets you utilize that.
They also offer non-cryptographic but crush-resistant variants that roughly double that performance. These algorithms parallelize really well and those advantages are even more significant on GPUs, where memory is at a premium and cryptographic acceleration is usually not available.
http://www.thesalmons.org/john/random123/papers/random123sc1...
http://www.thesalmons.org/john/random123/releases/1.06/docs/
In oversimplified pseudo-code:
def encrypt(data, key):
counter = 0
for block in data:
yield block ^ mix(counter, key)
counter += 1
def generate(key):
counter = 0
while True:
yield mix(counter, key)
counter += 1
def hash(data, key):
state = 0
for block in data:
state = block ^ mix(state, key)
return state
And similarly, if you have any one of those functions you can implement the rest. Xor with an unpredictable stream of numbers is a good encryption method, encrypting a string of zeroes should give you unpredictable random numbers and the hash can be used as the mix function when implementing the others, etc.When you encrypt something, the output is the same size as the input. The key is not mutated. You have to be able to go the other way if you know the key (for symmetric encryption).
When you hash something, the input is variable but the output length is fixed. Typically this is one-way - you should not be able to reconstruct the input for a given output.
When you have a stateful PRNG you get a fixed-length output but you have some internal state that gets changed with every call. So its inputs are generate(state) and you yield output and newstate. Going the other way is possible in theory but not typical.
The state for PRNGs is usually larger than the amount of output yielded, sometimes by quite a lot. E.g. Mersenne Twister has about 2.5 KB of state but yields only 32 bits of output per iteration. You generally want to store newstate every time, because setting them up is often expensive (MT takes a lot of calls before it's fully initialized and providing good randomness) and iterating through N-1 steps every time can take a very long time in a long-running program.
Usually you want PRNGs to be fast (but strongly random), but encryption and hashing should sometimes be slow (eg to protect passwords in a DB, or as a proof-of-work as in Bitcoin/Hashcash). It all depends on what role you're using them in.
You can sorta compose them into each other - like you can make a stateful PRNG by doing a hash on some internal state and then mixing some of it back into the state (eg like RDRAND). Or by encrypting a counter with a seed to make a PRNG (like Random123). Here be dragons, though - there's nothing guaranteeing your hand-rolled encryption algorithm will actually resist an attack, or whatever. So stay with a known, tested implementation.
More fundamentally it's keyed, and finding a way to make this work for an unkeyed hash is somewhat more complicated. But yes, I'm pretty sure that a secure stream cipher and a secure deterministic CSPRNG are basically the same thing.
Providing a "cryptographically secure but not really secure" alternative implemented in user space alongside a "really secure" alternative that's been thoroughly vetted would likely just add to the confusion. If you _really_ need secure numbers you should be using the really secure one. The really secure one is almost definitely going to require a syscall worth of overhead, at least, so it's _definitely_ going to be slower than a PRNG like xorshift128+ which literally takes a single digit number of CPU cycles.
Now to answer your question, if you're just talking about a CSPRNG algorithm they're probably fast enough for almost every use case. I think there's still an argument for some things like array shuffling, but those implementations could go out of their way to use the non-CS algorithm as an optimization. Unfortunately in Javascript land there's aren't many batteries included, so if you switched Math.random() to use a CSPRNG most people would end up using it for trivial stuff like shuffling arrays. If you're talking about a secure system... it's probably still fast enough for most things, but it's less obvious.
[1]http://sockpuppet.org/blog/2014/02/25/safely-generate-random...
The failures come when you try to seed in userspace. Not when you use urandom to seed a standard algorithm, with no customizations.
However, if we are going to decide as an industry that we need a userspace CSPRNG, it's not that hard of a job to write a single, high-quality, cross-platform implementation of a seeded-once CSPRNG that introduces no vulnerabilities beyond the kernel CSPRNG.
If the only access is a function that spits out a random number it's trivial to not let the state leak.
If you can't figure out how to open urandom securely to get the seed data, how are you going to use urandom every time you need a number?
If you use a secure n-bit block cipher in counter mode, for instance (generally considered a "good CSPRNG") then the generated numbers start to become distinguishable from random after 2^(n/2) generated blocks due to lack of collisions. This is a weak statement, but the point is any proofs that assume randomness (likely all proofs of hardness of crypto reversal) no longer hold. If you don't reseed before this point then you're doing it wrong. Other desirable characteristics of CSPRNGs, like forward secrecy, may require periodic (or frequent) reseeding depending on algorithm.
Best practice for a source of secure "randomness" is to use an entropy accumulator that finds as much "true entropy" as it can and constantly mixes that into a pool (while protecting against injection attacks), then to use a good CSPRNG algorithm to "stretch" the available entropy since true entropy is a scarce resource. That's what urandom and OpenSSL do, for instance (see Yarrow/Fortuna for a good design).
A lot of thought has been put into the design of urandom. Pulling one random seed from urandom and stretching it forever in user-space is _not_ equivalent. You could probably design something in user-space that _is_ as secure, but it's not trivial.
Adding in more is nice of the kernel but not necessary. If the pool doesn't already have enough entropy to last the lifetime of the computer, I would prefer not to trust it yet.
If you don't like the buffer overflow example, let's make it a misconfigured website that leaks the state in an error message or stack trace served to an end user.
On my current machine (Chrome 47, new-ish MacBook Pro), it takes me 4 seconds to run `for (var i = 0; i < 1000000; i++) {Math.random()}`. Each call to Math.random() generates an 8-byte floating point number, and it takes 0.6 seconds to `dd if=/dev/urandom bs=8 count=1000000 of=/dev/null`. If you don't want to buffer, and switch it to a million 8-byte reads, it goes up to 1.5 seconds. I don't think the system call overhead is a problem here.
mmalone$ node
> var f = function() {
... s = Date.now()
... for (var i = 0; i < 1000000; i++) {Math.random()}
... return Date.now() - s
... }
undefined
> f()
3
> f()
1
> f()
1
>
So I'm getting a couple milliseconds for 1,000,000 numbers. I'm also toying around with a PRNG package in Go currently so I have some numbers there too: BenchmarkXorshift128p-8 500000000 2.98 ns/op
BenchmarkXorshift1024s-8 300000000 4.62 ns/op
BenchmarkXorshift4096s-8 300000000 4.44 ns/op
BenchmarkXorshift4096ss-8 200000000 6.29 ns/op
BenchmarkUint64sFromCryptoRandIdiomatic-8 200000 10736 ns/op
BenchmarkUint64sFromCryptoRandUnsafe-8 200000 8081 ns/op
Go's crypto source pulls from urandom. This is more like what I'd expect in terms of performance. The difference is orders of magnitude, but we're still talking about microseconds so I think your argument is still valid. A proper CSPRNG is not "as fast" but it is probably "fast enough" for most use cases.But yeah, I think my underlying argument is roughly that if you do a naive / obvious shuffle of a list in JS using Math.floor(Math.random() * list.length), given the inherent overhead of manipulating a JS list, you aren't going to notice the overhead of a proper CSPRNG. I'm not totally sure if it's true, but I think it is likely to be true.
Safe defaults, make the developer go out of their way to use something less safe if they really know what they're doing.
Will renaming a function or two fix my bad behaviour?
The answer to "Should I use Math.random or X" is neither! I should be using a cryptography library like SJCL or tweetnacl-js:
Also, installed browser extensions are download-once and may need cryptographic functionality.
In some cases its impossibly/impractical to create a direct connection between 2 computers: you must use some kind of relay. If you don't want the relay to comprehend the data then you can use client side crypto on the payload. TLS still matters because it prevents 3rd parties from peeking at the communication.
Imagine a secure chat app where you want to guarantee that only you and your friend can read the communications, but want to be able to send offline messages to each other that get delivered upon connection.
One valid use, I think, could be around ensuring you don't pollute your backend with user data to avoid liability.
For example, if you wanted to use a distributed database in your backend and needed guarantees that when you delete data it is deleted everywhere at once, you could use JS crypto to do client-side encryption of the data and only have to delete the key to render the distributed data inert.
I heard about this through an article that was on HN a few weeks back (https://blog.balboa.io/yet-another.html)
https://developer.mozilla.org/en-US/docs/Web/API/RandomSourc...
Worst idea ever, this not-a-real-bug got a correction in just a few days without even being in the bug tracker, while there are real bugs stalled for years in the tracker. Writing a blog post and making a lot of noise on the internet works way better than using the bug tracker.
It's also worth noting that a good number of people (like [1]) outside of the V8 team were also skeptical because of lack of specification for the RNG, speed downsides to switching, and the availability of a CSPRNG in the browser and node. You can argue that they're wrong, but that's not immediately clear, Bug reports sometimes become that site of those arguments, and sometimes even the right arguments end up losing a round and having to come back in when the right people are noticing. Working as intended.
Regardless, the blog post or bug report dichotomy is a false one anyways, as obviously you can write a blog post and file a bug (as Mike later said he meant to). It's not like he was assailing the V8 team or something. The post was written a little dramatically, but he was engaged with the team in the followup bug (filed by a V8 dev) and elsewhere.
[1] https://mobile.twitter.com/sleevi_/status/667636624256344064
That's survivorship bias talking. How many blog posts and attempted noise never make any mark?
Better idea: file a bug and write the blog post. Yes, you won't always get all your issues looked at, but that's true of all major open source projects.
Quoting Donald Knuth / The Art of Computer Programming:
> “Many random number generators in use today are not very good. There is a tendency for people to avoid learning anything about such subroutines; quite often we find that some old method that is comparatively unsatisfactory has blindly been passed down from one programmer to another, and today’s users have no understanding of its limitations.”
Fortunately, when I went to college, my first CS prof loved TAOCP, and those were probably the first three CS texts I ever owned.
> A worth to read article by [Mike Malone](https://medium.com/@betable/tifu-by-using-math-random-f1c308...) explains the problem with Math.random() in more detail.
Probably the source of the shuffle (though it was not mentioned, its visualisation was used) considering the old implementation had been there for about 6 years (and had been partially busted until a few weeks ago)
This has been pointed out to us, and having understood the
problem and after some research, we decided to reimplement
Math.random based on an algorithm called xorshift128+.
Hyperlink in the first sentence.(Incidentally, the link is broken in your quote)
Hah you're right, completely missed the link (I'll put up a weak defence that gray visited links in gray text didn't help).
Thank you for the correction, noted in the original comment.
> (Incidentally, the link is broken in your quote)
Unsurprising, I just copy/pasted the section without fixing up the link. Fixed.
https://chromium.googlesource.com/v8/v8/+/2755c5a1b1cf7fc4c5...
//
// This class is used to generate a stream of pseudorandom numbers. The class
// uses a 48-bit seed, which is modified using a linear congruential formula.
// (See Donald Knuth, The Art of Computer Programming, Volume 3, Section 3.2.1.)
It's still not entirely clear why this particular algorithm was chosen (it'd been cool if some explanation had been given in this blog post). I suspect the reason was related to the fact that, in javascript, you can only multiply two 16 bit numbers without fear of loss of precision (without doing a fair amount of gymnastics), and they wanted something they could easily port into pure javascript to be used as a optimizer test. As partial support for that theory, the new code has now been put back almost entirely into the C++ layer.Then again, the author might be looking at that commit and wondering "Why did we pick that algorithm?" :)
That's about a different RNG than the one that Mike Malone's article was about. The one you're pointing to was used on the C++ side of the world (and so doesn't have the concerns of JS numbers), the one the article was about was pure JS.
Part of the V8 update was to unify the RNGs (see the patch history), so now both use the same (better) RNG. On the JS side, the new C++ RNG is now used to fill a buffer of random numbers to amortize the cost of calling into C++ code from JS.
As for why these implementations were chosen: Mike Malone covers the commit history and probable source of the JS implementation fairly well (which is probably all you'll really get since it's been 6 years or so now). The old C++ implementation is relatively recent, with some hints to its choosing here: https://github.com/v8/v8/commit/eb381b9444c6b1ec78414d1c9375...
You can do signed and unsigned 32-bit integer multiplication, but there's modulo/overflow there.
There you can see the code and watch it run. Neat!
The generator (LCG with multiplier 0x5DEECE66D) is not bad. Its main problem is that it has only 48 bits of state.
I'd think that having a "secure" random number generator isn't that important of a deal given the fact that all code runs client-side anyway (so why the need for cryptographic security?).
Yes! Designers of systems, APIs, and user interfaces should adopt this as a sort of mantra.
https://github.com/blixt/js-arbit
I would also recommend running the provided DieHarder test, which is crafted to measure the quality of PRNGs.
Were any professional experts on PRNGs asked for advice?
TL;DR long strings of repeated results are a sign of true randomness. Am I misinterpreting the relationship between that and this article?
TL;DR probability theory
(If you have free time and want to have some fun, throw a coin and draw a picture recording throws. I once generated a password by throwing a coin for more than hundred or times — each bit taking more than 1 throw to avoid biases — https://en.wikipedia.org/wiki/Fair_coin#Fair_results_from_a_...)
Not "on purpose", of course. But with a bad PRNG, the probability of 10 heads in a row is lower compared to a proper PRNG so in a sense the bad one is "evening out spectrum"
Computers are trying not to produce results different from fair coin flips.
The problem is highlighted by the images in this article:
http://jandemooij.nl/blog/2015/11/27/math-random-and-32-bit-...
When algorithms are getting tinkered with behind the scenes, this leads me to believe there's still way too much churn in the JS space.
There are applications in creative coding and gaming where it's critical to have a reproducible random sequence that gives the same results on all platforms.
That’s a hell of an oxymoron.
Edit Yes, I know obviously the intent was to refer to a pseudorandom sequence, but that’s not what was said. It’s also not what Math.random promises. It promises random-seeming output, nothing more, nothing less. It did not promise a cross-platform, reliably reproducible stream of pseudorandom numbers, and it would be in error to abuse a black-box "random" API by considering it to be a fully specified pseudorandom stream.