o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
406 lines • 17.2 kB
JavaScript
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