Based on this old answer, independently of the programming language (I'm using node.js with BigInt(), though), to generate the private spend key you should:

  1. Start with a seed (the same as the private spend key(?)):
const hexSeed = '198584347013dd91832be3d82529437db7cc8e1850e559cdd3872b29ca819601';
const decSeed = BigInt('0x'+hexSeed); //convert the hex seed to a dec BigInt: 11543724318435336731650421271699387718989312826353117660615550988399570687489
  1. Execute the Keccak-256 hash of the buffer of the hex seed:
const hexSeedKeccak = keccak256( Buffer.from( hexSeed, 'hex' ) ).toString('hex'); //889da12a88d36bce0966ab1a79125779dd1f2fc6f1145de131fd52a5b468796d, ok
const decSeedKeccak = BigInt('0x'+hexSeedKeccak); //convert the hex seed keccak to a dec BigInt: 61793054724383473727619989110790419561637989469961590701362921716625506335085
  1. Execute the sc_reduce32 function on the buffer (based on this and this sc_reduce32 is just decSeedKeccak % l where l = 2**252 + 27742317777372353535851937790883648493 or simply l = 7237005577332262213973186563042994240857116359379907606001950938285454250989):
const l = 7237005577332262213973186563042994240857116359379907606001950938285454250989n;
const decPrivateViewKey = decSeedKeccak % l; //3897010105725376015834496606446465634781058594922329853347314210341872327173
const hexPrivateViewKey = decPrivateViewKey.toString(16) //convert the dec BigInt to hex: 89da12a88d36bce0966ab1a79125779362760d1d957772e716a39d2ccb9da05

(Making it in little endian bytes results in 05DAB9CCD2396A712E7757D9D1602736795712791AAB6609CE6BD3882AA19D08)

But if you check the private view key calculated by skaht in the first link it doesn't match, while in the example of the 4th link it works fine using the method of using BigInt() with the % (mod) sign. When using the Cryptonote address tester sc_reduce32 function they both match to the example of the original answers. Getting deeper the function looks like:

function sc_reduce32(hex) {
  var input = hextobin(hex);
  if (input.length !== 32) {
    throw "Invalid input length";
  var mem = Module._malloc(32);
  Module.HEAPU8.set(input, mem);
  Module.ccall('sc_reduce32', 'void', ['number'], [mem]);
  var output = Module.HEAPU8.subarray(mem, mem + 32);
  return bintohex(output);

I understood (partly) how it works, but without understanding why it didn't simply use a modulo function:

  1. Get the binary version of the hexSeedKeccak
  2. Check if the binary has 32 digits, if not throw an error
  3. Call var mem = Module._malloc(32) (may be this):
    • var ptr = Runtime.dynamicAlloc(bytes+8) (still can't get what it does)
    • Return the AND bitwise operation of (5276816+8) & 4294967288
  4. Set the input in a Unit8Array with an offset of the memory of mem (probably related to this)
  5. Run Module.ccall (still can't get what it does)
  6. Gets the subarray with the 72 bits
  7. Frees the memory (?)
  8. Converts the binary back to hex

I would like to point out that at the 276th line of the oldMiniNero.py the function uses CURVE_P instead of l, where CURVE_P = (2**255 - 19) so

const CURVE_P = (2n**255n - 19n);
const decPrivateViewKey = decSeedKeccak % CURVE_P; //3897010105725376015834496606446465635002997137141308681634129712668941515136
const hexPrivateViewKey = decPrivateViewKey.toString(16) //convert the dec BigInt to hex: 89da12a88d36bce0966ab1a79125779dd1f2fc6f1145de131fd52a5b4687980

Which is still wrong. That is because the lines 280-281 in the comment says that

s is already in the form:
s[0]+256*s1+...+256^31*s[31] = s I can't really understand what it's meant to do, but I guess it's simply dividing into blocks and joining them literally into a binary string (I guess) of 32 digits (like step 1 of the js sc_reduce32 function)

So now I have a few question:

  1. Why doesn't the sc_reduce32 simply run a mod l and instead does some stuff with binary and memory? Is it related with the ed25519 library?
  2. Why is the hexPrivateViewKey 89da... instead of faa5... (wrong) when calculating the mod, but it works just fine with the example of the 4th link?
  3. Does it metter if I use the seed or the private spend key when generating, or could those two differ in some cases? Based this it should, is it true?

I would like to know if it's possible to make a function for this without having to import an external library (apart from the possible require('elliptic') and require('keccak256') ones). I'm new to monero, so, if you can, I would like a step-by-step explanation of the process of the sc_reduce32 function (possibly the concept for any programming language, even if js-specific is fine).


After knacc's answer I think I can post here the final result so people can understand how to do it. (I wrote it node.js-specific, but the logic is fine for any other programming-language)

  1. Firstly import the libraries:
//load the secure random integers generator algorithm
const secureRandom = require('secure-random');

//load the keccak256 algorithm
const keccak256 = require('keccak256');
  1. After that create the constants and functions
//sc_reduce32 algorithm function with any hex string
function sc_reduce32 (hex) {
    if (hex.length != 32) //if length isn't 32 throw error
        throw 'Input invalid length';
    return Buffer.from((BigInt('0x' + Buffer.from(hex, 'hex').reverse().toString('hex')) % l ).toString(16).padStart(64, '0'), 'hex').reverse(); //reverse (little endian), run mod l, pad from the start with '0', reverse (again) and get the hex.

//validate hex with simple regexp
function valid_hex (hex) {
    return new RegExp("[0-9a-fA-F]{" + hex.length + "}").test(hex);

//create constant l as big int
const l = 7237005577332262213973186563042994240857116359379907606001950938285454250989n
  1. Get the seed:
    • From first argument (if valid)
    • Randomly (if the first argument is not valid)
//check if 1st argument is valid for seed (even length, non-empty and valid hex)
const isArgumentValidSeed = process.argv[2]&&process.argv[2].length%2<1&&valid_hex(process.argv[2]);

//get the seed
const seed = isArgumentValidSeed?Buffer.from(process.argv[2], 'hex'):secureRandom.randomBuffer(32);
  1. Validate the seed
//if seed length is not 32 fast-hash it
const validSeed = seed.length != 32 ? keccak256(seed) : seed;
  1. Reduce the seed to the private spend key
//reduce validSeed to a valid 32-byte private spend key
const privateSpendKey = sc_reduce32(validSeed);
  1. If input seed length was 32 use validSeed, otherwise use the private spend key
//reduce the keccak-256 hash of validSeed (if the input seed length wasn't 32) or the private spend key (if the input seed length was 32)
const privateViewKey = sc_reduce32(keccak256(seed.length != 32 ? validSeed : privateSpendKey, 'hex'));

After that you can convert both keys to strings (they're stored as buffers)

1 Answer 1


const decSeed = BigInt('0x'+hexSeed); //convert the hex seed to a dec BigInt: 11543724318435336731650421271699387718989312826353117660615550988399570687489

You didn't interpret the hex here as little endian. The BigInt should have been 718235683823698183510320818008050772829110050800003465036123156060781708569

Why doesn't the sc_reduce32 simply run a mod l

You can do that too.

Does it metter if I use the seed or the private spend key when generating, or could those two differ in some cases?

Sometimes you'll see a seed that has not had sc_reduce32 applied, so you should always apply sc_reduce32 yourself. The seed is the same as the private spend key, as long as sc_reduce32 has been applied.

You'll find my javascript code here useful: https://github.com/knaccc/subaddress-js/blob/master/index.js (particularly the fastHash function).

  • Thank you a lot. I would like to pint out that you were right about the difference of seed and private key (seed = ffffffff840b46b1b5688edaf0782e3b7db7a12198d150958be7f41634b926e2 becomes privateSpendKey = 09698eea13a044e0fcd403f1c3cefc167cb7a12198d150958be7f41634b92602). I came up with this: k = keccak256(Buffer.from(seed.toString(16),'hex')).reverse().toString('hex') and then privateView = (BigInt('0x'+k)%l).toString(16).padStart(64,'0').split(/(.{2})/).filter(Boolean).reverse().join(''). I would still like to know if mod l always returns 63 characters and should be padded to 64.
    – DadiBit
    Commented Feb 26, 2020 at 13:21
  • If the seed is just a string like const seed = 'ffffffff840b46b1b5688edaf0782e3b7db7a12198d150958be7f41634b926e2' you can use this function function sc_reduce32 (hex) { return Buffer.from('0'+(BigInt('0x'+Buffer.from(hex,'hex').reverse().toString('hex'))%l).toString(16),'hex').reverse().toString('hex'); } to get the private spend key with sc_reduce(seed) and then use sc_reduce32(keccak256(Buffer.from(privateSpendKey,'hex'))) for the private view one.
    – DadiBit
    Commented Feb 26, 2020 at 14:27
  • @DadiBit mod l will return a number up to slightly over 2^252, which means that sometimes (but rarely) the result will be 64 hex characters, most of the time it'll be 63 characters, and sometimes it could be fewer characters. Therefore always normalize it to 64 characters before doing endian conversions.
    – knaccc
    Commented Feb 26, 2020 at 20:03
  • You're right. I will edit my question to add a few notes to better explain how it works. For now I use the .padStart(64, '0') function like Buffer.from((BigInt('0x' + Buffer.from(hex, 'hex').reverse().toString('hex')) % l ).toString(16).padStart(64, '0'), 'hex').reverse().toString('hex') where hex is the value (a string of a hex int) that we want to reduce
    – DadiBit
    Commented Feb 27, 2020 at 7:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.