A simple, good enough method is to apply SHA-256 to
- a per-user 128-bit secret full-entropy key
- a fixed-size constant pepper (constant for a given application, mildly confidential that is distributed only on need-to-know basis; distribution in code is not ideal, but better than no pepper, and there is little alternative.
- the user ID (can be variable-length)
and use the result as the 256-bit private key. An academically better option is SHA-3-256, or HMAC-SHA-256 with secret∥pepper the HMAC key, because that's a stronger keyed PRF.
Introducing the user ID is useful to prevent multi-target attack (attempting to find a private key matching any of many public keys). Without this, the expected effort for breaking one random private key among those of $n$ users is $2^{127}/n$ operations (hash, compute public key, find a match among the $n$ public keys), almost $n$ times lower than $2^{127}$ operations (hash, compute public key, compare to the one public key) with the user ID hashed.
Depending on perspective, what's proposed could be criticized as giving equivalent security as a full 256-bit random private key, or not:
- The best additional attack enabled is brute force search of the 128-bit seed, which requires an expected $2^{127}$ tests, each with a hash and some nontrivial ECC operation. That additional attack does not improve when attacking multiple targets. Other attack on ECC with 256-bit public key (Polard's Rho..) essentially breaking the discrete logarithm have comparable expected cost (like $2^{129}$) measured in number of some (other) elementary operations (I'm unsure about their multi-target status).
- But if we dive deeper: if we want that odds of success for an adversary doing a certain amount of work be below some low residual probability of break for a given of operations (say $\epsilon<2^{-30}$ for $2^{100}$ operations), then we have a problem: brute force search of the per-user 128-bit secret has $\epsilon\approx2^{128}/n=2^{-28}$ (not meeting our residual probability gooal), while for Pollard's Rho and friends $\epsilon\approx2^{257}/{n^2}=2^{-57}$ (acceptable with flying colors). And there is the additional, independent issue that perhaps the ECC cost for a brute force search can be lowered sizably with some pre-computations.
If we want an extra level of assurance, we can use an iterated hash function (PBKDF2, Bcrypt, Scrypt, Argon2, Balloon..) instead of a hash and get significant extra resistance to brute force search of the per-user 128-bit secret; like +20 bits of security for that attack.