UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

406 lines 17.2 kB
import { Field } from '../field.js'; import { Gadgets } from '../gadgets/gadgets.js'; import { assert } from '../../util/errors.js'; import { UInt8 } from '../int.js'; import { Bytes } from '../wrapped-classes.js'; import { bytesToWords, wordsToBytes } from '../gadgets/bit-slices.js'; export { Keccak }; const Keccak = { /** * Implementation of [NIST SHA-3](https://csrc.nist.gov/pubs/fips/202/final) Hash Function. * Supports output lengths of 256, 384, or 512 bits. * * Applies the SHA-3 hash function to a list of big-endian byte-sized {@link Field} elements, flexible to handle varying output lengths (256, 384, 512 bits) as specified. * * The function accepts {@link Bytes} as the input message, which is a type that represents a static-length list of byte-sized field elements (range-checked using {@link Gadgets.rangeCheck8}). * Alternatively, you can pass plain `number[]` of `Uint8Array` to perform a hash outside provable code. * * Produces an output of {@link Bytes} that conforms to the chosen bit length. * Both input and output bytes are big-endian. * * @param len - Desired output length in bits. Valid options: 256, 384, 512. * @param message - Big-endian {@link Bytes} representing the message to hash. * * ```ts * let preimage = Bytes.fromString("hello world"); * let digest256 = Keccak.nistSha3(256, preimage); * let digest384 = Keccak.nistSha3(384, preimage); * let digest512 = Keccak.nistSha3(512, preimage); * ``` * */ nistSha3(len, message) { return nistSha3(len, Bytes.from(message)); }, /** * Ethereum-Compatible Keccak-256 Hash Function. * This is a specialized variant of {@link Keccak.preNist} configured for a 256-bit output length. * * Primarily used in Ethereum for hashing transactions, messages, and other types of payloads. * * The function accepts {@link Bytes} as the input message, which is a type that represents a static-length list of byte-sized field elements (range-checked using {@link Gadgets.rangeCheck8}). * Alternatively, you can pass plain `number[]` of `Uint8Array` to perform a hash outside provable code. * * Produces an output of {@link Bytes} of length 32. Both input and output bytes are big-endian. * * @param message - Big-endian {@link Bytes} representing the message to hash. * * ```ts * let preimage = Bytes.fromString("hello world"); * let digest = Keccak.ethereum(preimage); * ``` */ ethereum(message) { return ethereum(Bytes.from(message)); }, /** * Implementation of [pre-NIST Keccak](https://keccak.team/keccak.html) hash function. * Supports output lengths of 256, 384, or 512 bits. * * Keccak won the SHA-3 competition and was slightly altered before being standardized as SHA-3 by NIST in 2015. * This variant was used in Ethereum before the NIST standardization, by specifying `len` as 256 bits you can obtain the same hash function as used by Ethereum {@link Keccak.ethereum}. * * The function applies the pre-NIST Keccak hash function to a list of byte-sized {@link Field} elements and is flexible to handle varying output lengths (256, 384, 512 bits) as specified. * * {@link Keccak.preNist} accepts {@link Bytes} as the input message, which is a type that represents a static-length list of byte-sized field elements (range-checked using {@link Gadgets.rangeCheck8}). * Alternatively, you can pass plain `number[]` of `Uint8Array` to perform a hash outside provable code. * * Produces an output of {@link Bytes} that conforms to the chosen bit length. * Both input and output bytes are big-endian. * * @param len - Desired output length in bits. Valid options: 256, 384, 512. * @param message - Big-endian {@link Bytes} representing the message to hash. * * ```ts * let preimage = Bytes.fromString("hello world"); * let digest256 = Keccak.preNist(256, preimage); * let digest384 = Keccak.preNist(384, preimage); * let digest512= Keccak.preNist(512, preimage); * ``` * */ preNist(len, message) { return preNist(len, Bytes.from(message)); }, }; // KECCAK CONSTANTS // Length of the square matrix side of Keccak states const KECCAK_DIM = 5; // Value `l` in Keccak, ranges from 0 to 6 and determines the lane width const KECCAK_ELL = 6; // Width of a lane of the state, meaning the length of each word in bits (64) const KECCAK_WORD = 2 ** KECCAK_ELL; // Number of bytes that fit in a word (8) const BYTES_PER_WORD = KECCAK_WORD / 8; // Length of the state in words, 5x5 = 25 const KECCAK_STATE_LENGTH_WORDS = KECCAK_DIM ** 2; // Length of the state in bits, meaning the 5x5 matrix of words in bits (1600) const KECCAK_STATE_LENGTH = KECCAK_STATE_LENGTH_WORDS * KECCAK_WORD; // Length of the state in bytes, meaning the 5x5 matrix of words in bytes (200) const KECCAK_STATE_LENGTH_BYTES = KECCAK_STATE_LENGTH / 8; // Creates the 5x5 table of rotation offset for Keccak modulo 64 // | i \ j | 0 | 1 | 2 | 3 | 4 | // | ----- | -- | -- | -- | -- | -- | // | 0 | 0 | 36 | 3 | 41 | 18 | // | 1 | 1 | 44 | 10 | 45 | 2 | // | 2 | 62 | 6 | 43 | 15 | 61 | // | 3 | 28 | 55 | 25 | 21 | 56 | // | 4 | 27 | 20 | 39 | 8 | 14 | const ROT_TABLE = [ [0, 36, 3, 41, 18], [1, 44, 10, 45, 2], [62, 6, 43, 15, 61], [28, 55, 25, 21, 56], [27, 20, 39, 8, 14], ]; // Round constants for Keccak // From https://keccak.team/files/Keccak-reference-3.0.pdf const ROUND_CONSTANTS = [ 0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n, 0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n, 0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an, 0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n, 0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an, 0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n, ]; // KECCAK HASH FUNCTION // Computes the number of required extra bytes to pad a message of length bytes function bytesToPad(rate, length) { return rate - (length % rate); } // Pads a message M as: // M || pad[x](|M|) // The padded message will start with the message argument followed by the padding rule (below) to fulfill a length that is a multiple of rate (in bytes). // If nist is true, then the padding rule is 0x06 ..0*..1. // If nist is false, then the padding rule is 10*1. function pad(message, rate, nist) { // Find out desired length of the padding in bytes // If message is already rate bits, need to pad full rate again const extraBytes = bytesToPad(rate, message.length); // 0x06 0x00 ... 0x00 0x80 or 0x86 const first = nist ? 0x06n : 0x01n; const last = 0x80n; // Create the padding vector const pad = Array(extraBytes).fill(UInt8.from(0)); pad[0] = UInt8.from(first); pad[extraBytes - 1] = pad[extraBytes - 1].add(last); // Return the padded message return [...message, ...pad]; } // ROUND TRANSFORMATION // First algorithm in the compression step of Keccak for 64-bit words. // C[i] = A[i,0] xor A[i,1] xor A[i,2] xor A[i,3] xor A[i,4] // D[i] = C[i-1] xor ROT(C[i+1], 1) // E[i,j] = A[i,j] xor D[i] // In the Keccak reference, it corresponds to the `theta` algorithm. // We use the first index of the state array as the i coordinate and the second index as the j coordinate. const theta = (state) => { const stateA = state; // XOR the elements of each row together // for all i in {0..4}: C[i] = A[i,0] xor A[i,1] xor A[i,2] xor A[i,3] xor A[i,4] const stateC = stateA.map((row) => row.reduce(xor)); // for all i in {0..4}: D[i] = C[i-1] xor ROT(C[i+1], 1) const stateD = Array.from({ length: KECCAK_DIM }, (_, i) => xor(stateC[(i + KECCAK_DIM - 1) % KECCAK_DIM], Gadgets.rotate64(stateC[(i + 1) % KECCAK_DIM], 1, 'left'))); // for all i in {0..4} and j in {0..4}: E[i,j] = A[i,j] xor D[i] const stateE = stateA.map((row, index) => row.map((elem) => xor(elem, stateD[index]))); return stateE; }; // Second and third steps in the compression step of Keccak for 64-bit words. // pi: A[i,j] = ROT(E[i,j], r[i,j]) // rho: A[i,j] = A'[j, 2i+3j mod KECCAK_DIM] // piRho: B[j,2i+3j] = ROT(E[i,j], r[i,j]) // which is equivalent to the `rho` algorithm followed by the `pi` algorithm in the Keccak reference as follows: // rho: // A[0,0] = a[0,0] // | i | = | 1 | // | j | = | 0 | // for t = 0 to 23 do // A[i,j] = ROT(a[i,j], (t+1)(t+2)/2 mod 64))) // | i | = | 0 1 | | i | // | | = | | * | | // | j | = | 2 3 | | j | // end for // pi: // for i = 0 to 4 do // for j = 0 to 4 do // | I | = | 0 1 | | i | // | | = | | * | | // | J | = | 2 3 | | j | // A[I,J] = a[i,j] // end for // end for // We use the first index of the state array as the i coordinate and the second index as the j coordinate. function piRho(state) { const stateE = state; const stateB = State.zeros(); // for all i in {0..4} and j in {0..4}: B[j,2i+3j] = ROT(E[i,j], r[i,j]) for (let i = 0; i < KECCAK_DIM; i++) { for (let j = 0; j < KECCAK_DIM; j++) { stateB[j][(2 * i + 3 * j) % KECCAK_DIM] = Gadgets.rotate64(stateE[i][j], ROT_TABLE[i][j], 'left'); } } return stateB; } // Fourth step of the compression function of Keccak for 64-bit words. // F[i,j] = B[i,j] xor ((not B[i+1,j]) and B[i+2,j]) // It corresponds to the chi algorithm in the Keccak reference. // for j = 0 to 4 do // for i = 0 to 4 do // A[i,j] = a[i,j] xor ((not a[i+1,j]) and a[i+2,j]) // end for // end for function chi(state) { const stateB = state; const stateF = State.zeros(); // for all i in {0..4} and j in {0..4}: F[i,j] = B[i,j] xor ((not B[i+1,j]) and B[i+2,j]) for (let i = 0; i < KECCAK_DIM; i++) { for (let j = 0; j < KECCAK_DIM; j++) { stateF[i][j] = xor(stateB[i][j], Gadgets.and( // We can use unchecked NOT because the length of the input is constrained to be 64 bits thanks to the fact that it is the output of a previous Xor64 Gadgets.not(stateB[(i + 1) % KECCAK_DIM][j], KECCAK_WORD, false), stateB[(i + 2) % KECCAK_DIM][j], KECCAK_WORD)); } } return stateF; } // Fifth step of the permutation function of Keccak for 64-bit words. // It takes the word located at the position (0,0) of the state and XORs it with the round constant. function iota(state, rc) { const stateG = state; stateG[0][0] = xor(stateG[0][0], Field.from(rc)); return stateG; } // One round of the Keccak permutation function. // iota o chi o pi o rho o theta function round(state, rc) { const stateA = state; const stateE = theta(stateA); const stateB = piRho(stateE); const stateF = chi(stateB); const stateD = iota(stateF, rc); return stateD; } // Keccak permutation function with a constant number of rounds function permutation(state, rcs) { return rcs.reduce((state, rc) => round(state, rc), state); } // KECCAK SPONGE // Absorb padded message into a keccak state with given rate and capacity function absorb(paddedMessage, capacity, rate, rc) { assert(rate + capacity === KECCAK_STATE_LENGTH_WORDS, `invalid rate or capacity (rate + capacity should be ${KECCAK_STATE_LENGTH_WORDS})`); assert(paddedMessage.length % rate === 0, 'invalid padded message length (should be multiple of rate)'); let state = State.zeros(); // array of capacity zero words const zeros = Array(capacity).fill(Field.from(0)); for (let idx = 0; idx < paddedMessage.length; idx += rate) { // split into blocks of rate words const block = paddedMessage.slice(idx, idx + rate); // pad the block with 0s to up to KECCAK_STATE_LENGTH_WORDS words const paddedBlock = block.concat(zeros); // convert the padded block to a Keccak state const blockState = State.fromWords(paddedBlock); // xor the state with the padded block const stateXor = State.xor(state, blockState); // apply the permutation function to the xored state state = permutation(stateXor, rc); } return state; } // Squeeze state until it has a desired length in words function squeeze(state, length, rate) { // number of squeezes const squeezes = Math.floor(length / rate) + 1; assert(squeezes === 1, 'squeezes should be 1'); // Obtain the hash selecting the first `length` words of the output array const words = State.toWords(state); const hashed = words.slice(0, length); return hashed; } // Keccak sponge function for 200 bytes of state width function sponge(paddedMessage, length, capacity, rate) { // check that the padded message is a multiple of rate assert(paddedMessage.length % rate === 0, 'Invalid padded message length'); // absorb const state = absorb(paddedMessage, capacity, rate, ROUND_CONSTANTS); // squeeze const hashed = squeeze(state, length, rate); return hashed; } // Keccak hash function with input message passed as list of Field bytes. // The message will be parsed as follows: // - the first byte of the message will be the least significant byte of the first word of the state (A[0][0]) // - the 10*1 pad will take place after the message, until reaching the bit length rate. // - then, {0} pad will take place to finish the 200 bytes of the state. function hash(message, length, capacity, nistVersion) { // Throw errors if used improperly assert(capacity > 0, 'capacity must be positive'); assert(capacity < KECCAK_STATE_LENGTH_BYTES, `capacity must be less than ${KECCAK_STATE_LENGTH_BYTES}`); assert(length > 0, 'length must be positive'); // convert capacity and length to word units assert(capacity % BYTES_PER_WORD === 0, 'length must be a multiple of 8'); capacity /= BYTES_PER_WORD; assert(length % BYTES_PER_WORD === 0, 'length must be a multiple of 8'); length /= BYTES_PER_WORD; const rate = KECCAK_STATE_LENGTH_WORDS - capacity; // apply padding, convert to words, and hash const paddedBytes = pad(message.bytes, rate * BYTES_PER_WORD, nistVersion); const padded = bytesToWords(paddedBytes); const hash = sponge(padded, length, capacity, rate); const hashBytes = wordsToBytes(hash); return hashBytes; } // Gadget for NIST SHA-3 function for output lengths 256/384/512. function nistSha3(len, message) { let bytes = hash(message, len / 8, len / 4, true); return BytesOfBitlength[len].from(bytes); } // Gadget for pre-NIST SHA-3 function for output lengths 256/384/512. // Note that when calling with output length 256 this is equivalent to the ethereum function function preNist(len, message) { let bytes = hash(message, len / 8, len / 4, false); return BytesOfBitlength[len].from(bytes); } // Gadget for Keccak hash function for the parameters used in Ethereum. function ethereum(message) { return preNist(256, message); } const State = { /** * Create a state of all zeros */ zeros() { return Array.from(Array(KECCAK_DIM), (_) => Array(KECCAK_DIM).fill(Field.from(0))); }, /** * Flatten state to words */ toWords(state) { const words = Array(KECCAK_STATE_LENGTH_WORDS); for (let j = 0; j < KECCAK_DIM; j++) { for (let i = 0; i < KECCAK_DIM; i++) { words[KECCAK_DIM * j + i] = state[i][j]; } } return words; }, /** * Compose words to state */ fromWords(words) { const state = State.zeros(); for (let j = 0; j < KECCAK_DIM; j++) { for (let i = 0; i < KECCAK_DIM; i++) { state[i][j] = words[KECCAK_DIM * j + i]; } } return state; }, /** * XOR two states together and return the result */ xor(a, b) { assert(a.length === KECCAK_DIM && a[0].length === KECCAK_DIM, `invalid \`a\` dimensions (should be ${KECCAK_DIM})`); assert(b.length === KECCAK_DIM && b[0].length === KECCAK_DIM, `invalid \`b\` dimensions (should be ${KECCAK_DIM})`); // Calls xor() on each pair (i,j) of the states input1 and input2 and outputs the output Fields as a new matrix return a.map((row, i) => row.map((x, j) => xor(x, b[i][j]))); }, }; // AUXILIARY TYPES class Bytes32 extends Bytes(32) { } class Bytes48 extends Bytes(48) { } class Bytes64 extends Bytes(64) { } const BytesOfBitlength = { 256: Bytes32, 384: Bytes48, 512: Bytes64, }; // xor which avoids doing anything on 0 inputs // (but doesn't range-check the other input in that case) function xor(x, y) { if (x.isConstant() && x.toBigInt() === 0n) return y; if (y.isConstant() && y.toBigInt() === 0n) return x; return Gadgets.xor(x, y, 64); } //# sourceMappingURL=keccak.js.map