mina-attestations
Version:
Private Attestations on Mina
160 lines (139 loc) • 5.7 kB
text/typescript
// the code in this file was copied and modified from o1js
// https://github.com/o1-labs/o1js
import { Bytes, Field, UInt32, UInt8 } from 'o1js';
import { assert, chunk, pad } from '../util.ts';
import { packBytes, unpackBytes } from './gadgets.ts';
import { Keccak } from './keccak-permutation.ts'; // TODO: import this from o1js once it's exported
import { DynamicArray } from './dynamic-array.ts';
import { StaticArray } from './static-array.ts';
import { DynamicString } from './dynamic-string.ts';
import { DynamicBytes } from './dynamic-bytes.ts';
export { DynamicSHA3 };
const DynamicSHA3 = {
/**
* Hash a dynamic-length byte array using the Ethereum-compatible Keccak-256 hash.
*
* Primarily used in Ethereum for hashing transactions, messages, and other types of payloads.
*
* The input type `DynamicArray<UInt8>` is compatible with both `DynamicString` and `DynamicBytes`:
*
* ```ts
* // using DynamicString
* const String = DynamicString({ maxLength: 120 });
* let string = String.from('hello');
* let hash = DynamicSHA3.keccak256(string);
*
* // using DynamicBytes
* const Bytes = DynamicBytes({ maxLength: 120 });
* let bytes = Bytes.fromHex('010203');
* let hash = DynamicSHA3.keccak256(bytes);
* ```
*/
keccak256(message: DynamicArray<UInt8> | Uint8Array | string): Bytes {
message = DynamicBytes.from(message);
let bytes = hash(message, {
length: 4, // 256 = 4*64 bits
capacity: 8, // 512 = 8*64 bits
isNist: false,
});
return Bytes32.from(bytes);
},
};
/**
* 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: DynamicArray<UInt8>,
options: { length: number; capacity: number; isNist: boolean }
): UInt8[] {
let rate = 25 - options.capacity;
// apply padding, convert to blocks of words
let blocks = padding(message, rate, options.isNist);
// absorb
let state = blocks.reduce(
Keccak.State,
Keccak.State.zeros(),
(state, block) => {
state = Keccak.State.xor(state, block);
return Keccak.permutation(state);
}
);
// squeeze once
// hash == first `length` words of the state
assert(options.length < rate, 'length should be less than rate');
let hash = Keccak.State.toWords(state).slice(0, options.length);
let hashBytes = wordsToBytes(hash);
return hashBytes;
}
/**
* 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 padding(
message: DynamicArray<UInt8>,
rate: number,
isNist: boolean
): DynamicArray<Field[][]> {
let rateBytes = rate * 8;
// convert message to blocks of `rate` 64-bit words each
const maxBlocksBytes = Math.ceil((message.maxLength + 1) / rateBytes);
const Block = StaticArray(UInt8, rateBytes);
const BlockDynamic = DynamicArray(UInt8, { maxLength: rateBytes });
const Blocks = DynamicArray(Block, { maxLength: maxBlocksBytes });
// number of actual blocks: ceil((message.length + 1) / rateBytes)
// = floor((message.length + 1 + (rateBytes - 1)) / rateBytes)
// = floor(message.length / rateBytes) + 1
// index of last block = blocks.length - 1 = floor(message.length / rate)
let { rest: messageLengthInLastBlock, quotient: lastBlockIndex } =
UInt32.Unsafe.fromField(message.length).divMod(rateBytes);
let numberOfBlocks = lastBlockIndex.value.add(1);
let padded = pad(message.array, maxBlocksBytes * rateBytes, UInt8.from(0));
let chunked = chunk(padded, rateBytes).map(Block.from);
let blocks = new Blocks(chunked, numberOfBlocks);
// padding is strictly contained the last block, so we operate on that to add padding
let lastBlock = blocks.getOrUnconstrained(lastBlockIndex.value);
let lastBlockDynamic = new BlockDynamic(
lastBlock.array,
messageLengthInLastBlock.value
);
// ensure that initial padding is all zeroes
lastBlockDynamic.normalize();
// add first padding byte
const first = isNist ? 0x06n : 0x01n;
lastBlockDynamic.setOrDoNothing(lastBlockDynamic.length, UInt8.from(first));
// add last padding byte (note: this could be the same as the first, so we use addition)
lastBlockDynamic.array[rateBytes - 1] = UInt8.Unsafe.fromField(
lastBlockDynamic.array[rateBytes - 1]!.value.add(0x80)
);
// now that we added padding to the last block, set it in the blocks array
blocks.setOrDoNothing(lastBlockIndex.value, lastBlock);
// pack UInt8 x rateBytes => UInt64 x rate
return blocks.map(Keccak.State, (blockBytes) => {
let block = bytesToWords(blockBytes.array);
// for convenience, each block is brought into the same shape as
// the state, by appending `capacity` zeros
let fullBlock = pad(block, 25, Field(0));
return Keccak.State.fromWords(fullBlock);
});
}
/**
* Convert an array of 64-bit Fields to an array of UInt8.
*/
function wordsToBytes(words: Field[]): UInt8[] {
return words.flatMap((w) => unpackBytes(w, 8));
}
/**
* Convert an array of UInt8 to an array of 64-bit Fields.
*/
function bytesToWords(bytes: UInt8[]): Field[] {
return chunk(bytes, 8).map((chunk) => packBytes(chunk));
}
class Bytes32 extends Bytes(32) {}