UNPKG

mina-attestations

Version:
404 lines (398 loc) 18.1 kB
import { Bytes, Field, Provable, Struct, UInt32, UInt64, UInt8 } from 'o1js'; import { DynamicArray } from "./dynamic-array.js"; import { StaticArray } from "./static-array.js"; import { assert, chunk, pad } from "../util.js"; import { SHA2 } from "./sha2.js"; import { uint64FromBytesBE, uint64ToBytesBE } from "./gadgets.js"; import { hashSafe } from "./dynamic-hash.js"; import { ProvableType, toFieldsPacked } from "../o1js-missing.js"; export { DynamicSHA2, Sha2IterationState, Sha2Iteration, Sha2FinalIteration, State32, State64, Block32, Block64, Bytes28, Bytes32, Bytes48, Bytes64, }; const DynamicSHA2 = { /** * Hash a dynamic-length byte array using different variants of SHA2. * * The first argument is the output length in bits (224, 256, 384, or 512). * * 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 = DynamicSHA2.hash(256, string); * * // using DynamicBytes * const Bytes = DynamicBytes({ maxLength: 120 }); * let bytes = Bytes.fromHex('010203'); * let hash = DynamicSHA2.hash(256, bytes); * ``` */ hash: sha2, /** * `DynamicSHA2.split()` is the first part of a more flexible API which allows to split proving a SHA2 hash over multiple proofs. * * Input arguments: * - `len`: the output length in bits (224, 256, 384, or 512) * - `blocksPerIteration`: how many SHA2 blocks (64-128 bytes) to process in each proof/iteration. Reasonable values are 2-8. * - `bytes`: the input bytes to hash * * `split()` is called **outside provable code** and prepares the inputs to the different proofs: * - `initial`: the initial "state" of the iteration (which is independent of the string to be hashed, and also returned by `Sha2IterationState.initial()`) * - `iterations`: a sequence of chunks (several blocks each) of the input string, to be passed to `update()` * - `final`: the last chunk of the input string, to be passed to `finalize()` * * Gist of how to use `split()`, `update()`, and `finalize()`: * ```ts * // outside the circuit: * const BLOCKS_PER_ITERATION = 6; * let { initial, iterations, final } = DynamicSHA2.split(256, BLOCKS_PER_ITERATION, bytes); * * // inside "update" circuit, for every iteration: * state = DynamicSHA2.update(initial, iterations[0]); * // OR * state = DynamicSHA2.update(state, iterations[i]); * * // inside "finalize" circuit: * let hash = DynamicSHA2.finalize(state, final, bytes); * ``` */ split, /** * `update()` is the second part of the API for splitting a SHA2 hash proof. * * It takes the current `Sha2IterationState` and a `Sha2Iteration` (a chunk of blocks to be hashed) and returns the updated state. * * See `split()` for additional details. */ update, /** * `finalize()` is the last part of the API for splitting a SHA2 hash proof. * * It takes the current `Sha2IterationState`, a `Sha2FinalIteration`, and the original input bytes, and returns the hash. * Since the `Sha2IterationState` contains a commitment to the previous blocks that were hashed, calling `finalize()` is able * to prove that the same input bytes were hashed across multiple iterations. Thus, after calling it you are able to use * the same input bytes in further statements. * * See `split()` for additional details. */ finalize, // low-level API padding256, padding512, commitBlock256, commitBlock512, hashBlock256, hashBlock512, initialState256: (l) => State32.from(SHA2.initialState256(l)), initialState512: (l) => State64.from(SHA2.initialState512(l)), }; function sha2(len, bytes) { if (len === 224 || len === 256) return hash256(len, bytes); if (len === 384 || len === 512) return hash512(len, bytes); throw new Error('unsupported hash length'); } // static array types for blocks / state / result class UInt8x4 extends StaticArray(UInt8, 4) { } class UInt8x8 extends StaticArray(UInt8, 8) { } class UInt8x64 extends StaticArray(UInt8, 64) { } class UInt8x128 extends StaticArray(UInt8, 128) { } class Block32 extends StaticArray(UInt32, 16) { } class State32 extends StaticArray(UInt32, 8) { } class Block64 extends StaticArray(UInt64, 16) { } class State64 extends StaticArray(UInt64, 8) { } const Bytes28 = Bytes(28); const Bytes32 = Bytes(32); const Bytes48 = Bytes(48); const Bytes64 = Bytes(64); function hash256(len, bytes) { let blocks = padding256(bytes); // hash a dynamic number of blocks using DynamicArray.reduce() let state = blocks.reduce(State32, State32.from(SHA2.initialState256(len)), (state, block) => { let W = SHA2.messageSchedule256(block.array); return State32.from(SHA2.compression256(state.array, W)); }); if (len === 224) state = state.slice(0, 7); let result = state.array.flatMap((x) => x.toBytesBE()); return len === 224 ? Bytes28.from(result) : Bytes32.from(result); } function hash512(len, bytes) { let blocks = padding512(bytes); // hash a dynamic number of blocks using DynamicArray.reduce() let state = blocks.reduce(State64, State64.from(SHA2.initialState512(len)), (state, block) => { let W = SHA2.messageSchedule512(block.array); return State64.from(SHA2.compression512(state.array, W)); }); if (len === 384) state = state.slice(0, 6); let result = state.array.flatMap((x) => uint64ToBytesBE(x)); return len === 384 ? Bytes48.from(result) : Bytes64.from(result); } /** * Apply padding to dynamic-length input bytes and convert them to (a dynamic number of) blocks of 16 uint32s. */ function padding256(message) { /* padded message looks like this: M ... M 0x80 0x0 ... 0x0 L L L L L L L L where - M is the original message - the 8 L bytes encode the length of the original message, as a uint64 - padding always starts with a 0x80 byte (= big-endian encoding of 1) - there are k 0x0 bytes, where k is the smallest number such that the padded length (in bytes) is a multiple of 64 Corollaries: - the entire L section is always contained at the end of the last block - the 0x80 byte might be in the last block or the one before that - max number of blocks = ceil((M.maxLength + 9) / 64) - number of actual blocks = ceil((M.length + 9) / 64) = floor((M.length + 9 + 63) / 64) = floor((M.length + 8) / 64) + 1 - block number of L section = floor((M.length + 8) / 64) - block number of 0x80 byte index = floor(M.length / 64) */ // check that all message bytes beyond the actual length are 0, so that we get valid padding just by adding the 0x80 and L bytes // this step creates most of the constraint overhead of dynamic sha2, but seems unavoidable :/ message.forEach((byte, isPadding) => { Provable.assertEqualIf(isPadding, UInt8, byte, UInt8.from(0)); }); // create blocks of 64 bytes each const maxBlocks = Math.ceil((message.maxLength + 9) / 64); const BlocksOfBytes = DynamicArray(UInt8x64, { maxLength: maxBlocks }); let lastBlockIndex = UInt32.Unsafe.fromField(message.length.add(8)).div(64); let numberOfBlocks = lastBlockIndex.value.add(1); let padded = pad(message.array, maxBlocks * 64, UInt8.from(0)); let chunked = chunk(padded, 64).map(UInt8x64.from); let blocksOfBytes = new BlocksOfBytes(chunked, numberOfBlocks); // pack each block of 64 bytes into 16 uint32s (4 bytes each) let blocks = blocksOfBytes.map(Block32, (block) => block.chunk(4).map(UInt32, (b) => UInt32.fromBytesBE(b.array))); // splice the length in the same way // length = l0 + 4*l1 + 64*l2 // so that l2 is the block index, l1 the uint32 index in the block, and l0 the byte index in the uint32 let [l0, l1, l2] = splitMultiIndex(UInt32.Unsafe.fromField(message.length)); // hierarchically get byte at `length` and set to 0x80 // we can use unsafe get/set because the indices are in bounds by design let block = blocks.getOrUnconstrained(l2); let uint8x4 = UInt8x4.from(block.getOrUnconstrained(l1).toBytesBE()); uint8x4.setOrDoNothing(l0, UInt8.from(0x80)); block.setOrDoNothing(l1, UInt32.fromBytesBE(uint8x4.array)); blocks.setOrDoNothing(l2, block); // set last 64 bits to encoded length (in bits, big-endian encoded) // in fact, since dynamic array asserts that length fits in 16 bits, we can set the second to last uint32 to 0 let lastBlock = blocks.getOrUnconstrained(lastBlockIndex.value); lastBlock.set(14, UInt32.from(0)); lastBlock.set(15, UInt32.Unsafe.fromField(message.length.mul(8))); // length in bits blocks.setOrDoNothing(lastBlockIndex.value, lastBlock); return blocks; } function splitMultiIndex(index) { let { rest: l0, quotient: l1 } = index.divMod(64); let { rest: l00, quotient: l01 } = l0.divMod(4); return [l00.value, l01.value, l1.value]; } /** * Apply padding to dynamic-length input bytes and convert them to (a dynamic number of) blocks of 16 uint64s. */ function padding512(message) { /* padded message looks like this: M ... M 0x80 0x0 ... 0x0 [...L[16]] where - M is the original message - the 16 L bytes encode the length of the original message, as a uint128 - padding always starts with a 0x80 byte (= big-endian encoding of 1) - there are k 0x0 bytes, where k is the smallest number such that the padded length (in bytes) is a multiple of 128 Corollaries: - the entire L section is always contained at the end of the last block - the 0x80 byte might be in the last block or the one before that - max number of blocks = ceil((M.maxLength + 17) / 128) - number of actual blocks = ceil((M.length + 17) / 128) = floor((M.length + 17 + 127) / 128) = floor((M.length + 16) / 128) + 1 - block number of L section = floor((M.length + 16) / 128) - block number of 0x80 byte index = floor(M.length / 128) */ // check that all message bytes beyond the actual length are 0, so that we get valid padding just by adding the 0x80 and L bytes // this step creates most of the constraint overhead of dynamic sha2, but seems unavoidable :/ message.forEach((byte, isPadding) => { Provable.assertEqualIf(isPadding, UInt8, byte, UInt8.from(0)); }); // create blocks of 128 bytes each const maxBlocks = Math.ceil((message.maxLength + 17) / 128); const BlocksOfBytes = DynamicArray(UInt8x128, { maxLength: maxBlocks }); let lastBlockIndex = UInt32.Unsafe.fromField(message.length.add(16)).div(128); let numberOfBlocks = lastBlockIndex.value.add(1); let padded = pad(message.array, maxBlocks * 128, UInt8.from(0)); let chunked = chunk(padded, 128).map(UInt8x128.from); let blocksOfBytes = new BlocksOfBytes(chunked, numberOfBlocks); // pack each block of 128 bytes into 16 uint64s (8 bytes each) let blocks = blocksOfBytes.map(Block64, (block) => block.chunk(8).map(UInt64, (b) => uint64FromBytesBE(b.array))); // splice the length in the same way // length = l0 + 8*l1 + 128*l2 // so that l2 is the block index, l1 the uint64 index in the block, and l0 the byte index in the uint64 let [l0, l1, l2] = splitMultiIndex64(UInt32.Unsafe.fromField(message.length)); // hierarchically get byte at `length` and set to 0x80 // we can use unsafe get/set because the indices are in bounds by design let block = blocks.getOrUnconstrained(l2); let uint8x8 = UInt8x8.from(uint64ToBytesBE(block.getOrUnconstrained(l1))); uint8x8.setOrDoNothing(l0, UInt8.from(0x80)); block.setOrDoNothing(l1, uint64FromBytesBE(uint8x8.array)); blocks.setOrDoNothing(l2, block); // set last 128 bits to encoded length (in bits, big-endian encoded) // in fact, since dynamic array asserts that length fits in 16 bits, we can set the second to last uint64 to 0 let lastBlock = blocks.getOrUnconstrained(lastBlockIndex.value); lastBlock.set(14, UInt64.from(0)); lastBlock.set(15, UInt64.Unsafe.fromField(message.length.mul(8))); // length in bits blocks.setOrDoNothing(lastBlockIndex.value, lastBlock); return blocks; } function splitMultiIndex64(index) { let { rest: l0, quotient: l1 } = index.divMod(128); let { rest: l00, quotient: l01 } = l0.divMod(8); return [l00.value, l01.value, l1.value]; } function initialState(len) { if (len === 224 || len === 256) { return { len, state: State32.from(SHA2.initialState256(len)), commitment: Field(0), }; } else { return { len, state: State64.from(SHA2.initialState512(len)), commitment: Field(0), }; } } function split(len, blocksPerIteration, bytes) { let initial = initialState(len); if (len === 224 || len === 256) { let blocks = padding256(bytes); let [iterations, final] = blocks.chunk(blocksPerIteration); return { initial, iterations: iterations.array .slice(0, Number(iterations.length)) .map((blocks) => ({ type: 256, blocks })), final: { type: 256, blocks: final }, }; } else { let blocks = padding512(bytes); let [iterations, final] = blocks.chunk(blocksPerIteration); return { initial, iterations: iterations.array .slice(0, Number(iterations.length)) .map((blocks) => ({ type: 512, blocks })), final: { type: 512, blocks: final }, }; } } function update(iterState, iteration) { if (iterState.len === 224 || iterState.len === 256) { assert(iteration.type === 256, 'incompatible types'); // update hash state and commitment let { state, commitment } = iterState; state = iteration.blocks.reduce(state, hashBlock256); commitment = iteration.blocks.reduce(commitment, commitBlock256); return { len: iterState.len, state, commitment }; } else { assert(iterState.len === 384 || iterState.len === 512, 'invalid state'); assert(iteration.type === 512, 'incompatible types'); // update hash state and commitment let { state, commitment } = iterState; state = iteration.blocks.reduce(state, hashBlock512); commitment = iteration.blocks.reduce(commitment, commitBlock512); return { len: iterState.len, state, commitment }; } } function finalize(iterState, final, bytes) { if (iterState.len === 224 || iterState.len === 256) { assert(final.type === 256, 'incompatible types'); // update hash state and commitment let { state, commitment } = iterState; state = final.blocks.reduce(State32, state, hashBlock256); commitment = final.blocks.reduce(Field, commitment, commitBlock256); // recompute commitment from scratch to confirm we really hashed the input bytes let expected = padding256(bytes).reduce(Field, Field(0), commitBlock256); commitment.assertEquals(expected, 'invalid commitment'); // finalize hash let result = state.array.flatMap((x) => x.toBytesBE()); return iterState.len === 224 ? Bytes28.from(result) : Bytes32.from(result); } else { assert(iterState.len === 384 || iterState.len === 512, 'invalid state'); assert(final.type === 512, 'incompatible types'); // update hash state and commitment let { state, commitment } = iterState; state = final.blocks.reduce(State64, state, hashBlock512); commitment = final.blocks.reduce(Field, commitment, commitBlock512); // recompute commitment from scratch to confirm we really hashed the input bytes let expected = padding512(bytes).reduce(Field, Field(0), commitBlock512); commitment.assertEquals(expected, 'invalid commitment'); // finalize hash let result = state.array.flatMap((x) => uint64ToBytesBE(x)); return iterState.len === 384 ? Bytes48.from(result) : Bytes64.from(result); } } // provable types for update API function Sha2IterationState(len) { const S = Struct({ len: ProvableType.constant(len), state: State(len), commitment: Field, }); return Object.assign(S, { initial() { return initialState(len); }, }); } Sha2IterationState.initial = initialState; function Sha2Iteration(len, blocksPerIteration) { return Struct({ type: ProvableType.constant(len === 224 || len === 256 ? 256 : 512), blocks: StaticArray(Block(len), blocksPerIteration), }); } function Sha2FinalIteration(len, blocksPerIteration) { return Struct({ type: ProvableType.constant(len === 224 || len === 256 ? 256 : 512), blocks: DynamicArray(Block(len), { maxLength: blocksPerIteration }), }); } // helpers for update API function hashBlock256(state, block) { let W = SHA2.messageSchedule256(block.array); return State32.from(SHA2.compression256(state.array, W)); } function hashBlock512(state, block) { let W = SHA2.messageSchedule512(block.array); return State64.from(SHA2.compression512(state.array, W)); } // poseidon hash for keeping a commitment to the blocks that were hashed function commitBlock256(commitment, block) { let blockHash = hashSafe(toFieldsPacked(Block32, block)); return hashSafe([commitment, blockHash]); } function commitBlock512(commitment, block) { let blockHash = hashSafe(toFieldsPacked(Block64, block)); return hashSafe([commitment, blockHash]); } function Block(len) { return len === 224 || len === 256 ? Block32 : Block64; } function State(len) { return len === 224 || len === 256 ? State32 : State64; } //# sourceMappingURL=dynamic-sha2.js.map