Skip to content

Instantly share code, notes, and snippets.

@0xSachinK
Last active September 28, 2023 23:00
Show Gist options
  • Save 0xSachinK/7702f9c2225e732376235369712acf80 to your computer and use it in GitHub Desktop.
Save 0xSachinK/7702f9c2225e732376235369712acf80 to your computer and use it in GitHub Desktop.
Variable length SHA256 Hash

Variable Length SHA256 Hash

Problem Statement: https://gist.github.com/puma314/6bf27edd39a30419f8feeee884f41488

Solution

Open this in zkREPL →

Preprocess input

Convert two dimenstion array of input bytes to one large concatenated array of input bits. Transform input_bytes[10][32] to all_bits[10 * 32 * 8].

    // Convert to bits and construct one large array
    var numInputBytes = n * 32;
    signal all_bits[numInputBytes * 8];
    component num2Bits[numInputBytes];
    for (var i = 0; i < n; i++) {
        for (var j = 0; j < 32; j++) {
            num2Bits[i * 32 + j] = Num2Bits(8);
            num2Bits[i * 32 + j].in <== input_bytes[i][j];
            for (var k = 0; k < 8; k++) {
                all_bits[(i * 32 + j) * 8 + k] <== num2Bits[i * 32 + j].out[7 - k];
            }
        }
    }

Construct padded input arrays

SHA256 requires inputs to be padded using the following scheme:

INPUT_BITS || 1 || 0....0 || LENGTH_OF_INPUT_BITS

Where LENGTH_OF_INPUT_BITS is stored in the last 64 bits. The total length of padding is a multiple of 512 bits.

Now, given that length signal is not defined at compile time, we will need to construct a padded input array for each possible value of length. But we can only feed in a fixed length array to the hash function. So, we will pad the input array with zeroes such that the length of all of them is the same as the length of the padded array of length 10, which is 3072 bits. We will see later how we can avoid hashing the padded zeroes.

So, for length = 1, we will have:

First 256 bits of all_bits || 1 || 0....0 || 256_in_bits || 0...0 until length is 3072

for length = 2, we will have:

First 512 bits of all_bits || 1 || 0....0 || 512_in_bits || 0...0 until length is 3072

and so on.

    // Construct arrays with inputs to the hash functions for all lengths
    var maxBlocks = 6;
    var maxBits = 6 * 512;   // 3072

    signal padded_input[n][maxBits];
    component padders[n];

    for (var i = 0; i < n; i++) {

        var inp_size = 256 * (i + 1);
        padders[i] = Sha256Padding(inp_size);

        for (var t = 0; t < inp_size; t++) {
            padders[i].in[t] <== all_bits[t];
        }

        var nBlocks = ((i + 1) \ 2) + 1;
        for (var t = 0; t < nBlocks * 512; t++) {
            padded_input[i][t] <== padders[i].out[t];
        }

        // Pad the rest with zeroes
        for (var t = nBlocks * 512; t < maxBits; t++) {
            padded_input[i][t] <== 0;
        }
    }

Select input based on length

Next out of all the possible 10 padded inputs to the, we select one of them by first constructing a mask and then applying it.

    // Select input to hash function based on length

    // Create a mask with 1 at index length and 0 otherwise
    signal mask[n];
    component isEq[n];
    for (var i = 0; i < n; i++) {
        isEq[i] = IsEqual();
        isEq[i].in[0] <== i + 1;
        isEq[i].in[1] <== length;
        mask[i] <== isEq[i].out;
    }

    // Apply the mask to select the input array to be fed into Sha256 hash function
    signal sha_inp[maxBits];
    signal sum[maxBits][n];
    for (var i = 0; i < maxBits; i++) {
        sum[i][0] <== mask[0] * padded_input[0][i];
        for (var j = 1; j < n; j++) {
            sum[i][j] <== sum[i][j-1]  + mask[j] *  padded_input[j][i];
        }
        sha_inp[i] <== sum[i][n-1];
    }

Hashing the input

Custom SHA256 Template

This custom SHA256 template leverages the Merkle-Damgard construction of SHA256 function. The SHA256 hash function applies the same compression function to each block of 512 bits of the input. The output of the compression function is then fed into the next compression function along with the next block of 512 bits of the input.

Now, our padded input contains a valid padded input and then a bunch of zeroes. For example, for length 2, we have the following:

First 512 bits of all_bits || 1 || 0....0 || 512_in_bits || 0...0 until length is 3072

We want to avoid hasing the padded zeroes. So, we will hash all the bits, but then we will only select the output of the compression function corresponding to the block that contains the valid input. For example, for length 2, we will hash all the bits, but then we will only select the output of the compression function corresponding to the second block, which has hashed values of the first 1024 bits of the input, which is this

First 512 bits of all_bits || 1 || 0....0 || 512_in_bits

and we discard the output of the rest of the compression functions.

Implementation of the CustomSha256 template is as follows:

template CustomSha256(nBlocks) {

    var i;
    var k;

    signal input paddedIn[nBlocks*512];
    signal input output_block;
    signal output out[256];

    /*
    Usual SHA256 stuff...
    */

    // Create a mask with 1 at output block and 0 otherwise
    signal mask[nBlocks];
    component isEq[nBlocks];
    for (var j = 0; j < nBlocks; j++) {
        isEq[j] = IsEqual();
        isEq[j].in[0] <== (j + 1);
        isEq[j].in[1] <== output_block;
        mask[j] <== isEq[j].out;
    }

    // Apply mask to select the output from output_block
    signal sum[256][nBlocks];
    for (k=0; k<256; k++) {
        sum[k][0] <== mask[0] * sha256compression[0].out[k];
        for (var j = 1; j < nBlocks; j++) {
            sum[k][j] <== sum[k][j-1]  + mask[j] *  sha256compression[j].out[k];
        }
        out[k] <== sum[k][nBlocks - 1];
    }

}

Feeding input to SHA256

We feed the selected input to the SHA256 hash function. We determine the sha256 compression function (ouptut_block) from which we want to extract the output based on the length signal.

    // Feed sha_inp to SHA256
    signal q <-- length \ 2;
    signal rem <-- length % 2;
    // Make sure rem is < 2, i.e. rem is 0 or 1
    rem * (rem - 1) === 0;
    q * 2 + rem === length;

    component sha256 = CustomSha256(maxBlocks);
    for (var i = 0; i < maxBits; i++) {
        sha256.paddedIn[i] <== sha_inp[i];
    }
    sha256.output_block <== q + 1;

    // Convert output of sha256 to array of bytes
    component bits2Num[32];
    for (var i = 0; i < 32; i++) {
        bits2Num[i] = Bits2Num(8);
        for (var j = 0; j < 8; j++) {
            bits2Num[i].in[j] <== sha256.out[i * 8 + j];
        }
        out[i] <== bits2Num[i].out;
    }

Open this in zkREPL →

This file can be included into other zkREPLs with include "gist:7702f9c2225e732376235369712acf80";

pragma circom 2.1.4;
include "circomlib/bitify.circom";
include "circomlib/comparators.circom";
include "circomlib/sha256/constants.circom";
include "circomlib/sha256/sha256compression.circom";
template Sha256Padding(nBits) {
signal input in[nBits];
var i;
var k;
var nBlocks;
nBlocks = ((nBits + 64)\512)+1;
signal output out[nBlocks*512];
for (k=0; k<nBits; k++) {
out[k] <== in[k];
}
out[nBits] <== 1;
for (k=nBits+1; k<nBlocks*512-64; k++) {
out[k] <== 0;
}
for (k = 0; k< 64; k++) {
out[nBlocks*512 - k -1] <== (nBits >> k)&1;
}
}
template CustomSha256(nBlocks) {
var i;
var k;
signal input paddedIn[nBlocks*512];
signal input output_block;
signal output out[256];
component ha0 = H(0);
component hb0 = H(1);
component hc0 = H(2);
component hd0 = H(3);
component he0 = H(4);
component hf0 = H(5);
component hg0 = H(6);
component hh0 = H(7);
component sha256compression[nBlocks];
for (i=0; i<nBlocks; i++) {
sha256compression[i] = Sha256compression() ;
if (i==0) {
for (k=0; k<32; k++ ) {
sha256compression[i].hin[0*32+k] <== ha0.out[k];
sha256compression[i].hin[1*32+k] <== hb0.out[k];
sha256compression[i].hin[2*32+k] <== hc0.out[k];
sha256compression[i].hin[3*32+k] <== hd0.out[k];
sha256compression[i].hin[4*32+k] <== he0.out[k];
sha256compression[i].hin[5*32+k] <== hf0.out[k];
sha256compression[i].hin[6*32+k] <== hg0.out[k];
sha256compression[i].hin[7*32+k] <== hh0.out[k];
}
} else {
for (k=0; k<32; k++ ) {
sha256compression[i].hin[32*0+k] <== sha256compression[i-1].out[32*0+31-k];
sha256compression[i].hin[32*1+k] <== sha256compression[i-1].out[32*1+31-k];
sha256compression[i].hin[32*2+k] <== sha256compression[i-1].out[32*2+31-k];
sha256compression[i].hin[32*3+k] <== sha256compression[i-1].out[32*3+31-k];
sha256compression[i].hin[32*4+k] <== sha256compression[i-1].out[32*4+31-k];
sha256compression[i].hin[32*5+k] <== sha256compression[i-1].out[32*5+31-k];
sha256compression[i].hin[32*6+k] <== sha256compression[i-1].out[32*6+31-k];
sha256compression[i].hin[32*7+k] <== sha256compression[i-1].out[32*7+31-k];
}
}
for (k=0; k<512; k++) {
sha256compression[i].inp[k] <== paddedIn[i*512+k];
}
}
// Create a mask with 1 at output block and 0 otherwise
signal mask[nBlocks];
component isEq[nBlocks];
for (var j = 0; j < nBlocks; j++) {
isEq[j] = IsEqual();
isEq[j].in[0] <== (j + 1);
isEq[j].in[1] <== output_block;
mask[j] <== isEq[j].out;
}
// Apply mask to select the output from output_block
signal sum[256][nBlocks];
for (k=0; k<256; k++) {
sum[k][0] <== mask[0] * sha256compression[0].out[k];
for (var j = 1; j < nBlocks; j++) {
sum[k][j] <== sum[k][j-1] + mask[j] * sha256compression[j].out[k];
}
out[k] <== sum[k][nBlocks - 1];
}
}
template VariableLengthHash (n) {
signal input input_bytes[n][32];
signal input length;
signal output out[32];
// Convert to bits and construct one large array
var numInputBytes = n * 32;
signal all_bits[numInputBytes * 8];
component num2Bits[numInputBytes];
for (var i = 0; i < n; i++) {
for (var j = 0; j < 32; j++) {
num2Bits[i * 32 + j] = Num2Bits(8);
num2Bits[i * 32 + j].in <== input_bytes[i][j];
for (var k = 0; k < 8; k++) {
all_bits[(i * 32 + j) * 8 + k] <== num2Bits[i * 32 + j].out[7 - k];
}
}
}
// Construct arrays with inputs to the hash functions for all lengths
var maxBlocks = 6;
var maxBits = 6 * 512; // 3072
signal padded_input[n][maxBits];
component padders[n];
for (var i = 0; i < n; i++) {
var inp_size = 256 * (i + 1);
padders[i] = Sha256Padding(inp_size);
for (var t = 0; t < inp_size; t++) {
padders[i].in[t] <== all_bits[t];
}
var nBlocks = ((i + 1) \ 2) + 1;
for (var t = 0; t < nBlocks * 512; t++) {
padded_input[i][t] <== padders[i].out[t];
}
// Pad the rest with zeroes
for (var t = nBlocks * 512; t < maxBits; t++) {
padded_input[i][t] <== 0;
}
}
// Select input to hash function based on length
// Create a mask with 1 at index length and 0 otherwise
signal mask[n];
component isEq[n];
for (var i = 0; i < n; i++) {
isEq[i] = IsEqual();
isEq[i].in[0] <== i + 1;
isEq[i].in[1] <== length;
mask[i] <== isEq[i].out;
}
// Apply the mask to select the input array to be fed into Sha256 hash function
signal sha_inp[maxBits];
signal sum[maxBits][n];
for (var i = 0; i < maxBits; i++) {
sum[i][0] <== mask[0] * padded_input[0][i];
for (var j = 1; j < n; j++) {
sum[i][j] <== sum[i][j-1] + mask[j] * padded_input[j][i];
}
sha_inp[i] <== sum[i][n-1];
}
// Feed sha_inp to SHA256
signal q <-- length \ 2;
signal rem <-- length % 2;
// Make sure rem is < 2, i.e. rem is 0 or 1
rem * (rem - 1) === 0;
q * 2 + rem === length;
component sha256 = CustomSha256(maxBlocks);
for (var i = 0; i < maxBits; i++) {
sha256.paddedIn[i] <== sha_inp[i];
}
sha256.output_block <== q + 1;
// Convert output of sha256 to array of bytes
component bits2Num[32];
for (var i = 0; i < 32; i++) {
bits2Num[i] = Bits2Num(8);
for (var j = 0; j < 8; j++) {
bits2Num[i].in[j] <== sha256.out[i * 8 + j];
}
out[i] <== bits2Num[i].out;
}
// Print for convenience
component b2n = Bits2Num(256);
b2n.in <== sha256.out;
log(b2n.out);
}
component main { public [ input_bytes, length ] } = VariableLengthHash(10);
/* INPUT = {
"input_bytes": [
["46", "138", "238", "88", "166", "127", "203", "83", "111", "224", "135", "34", "165", "136", "172", "250", "191", "241", "243", "198", "142", "194", "81", "164", "55", "14", "123", "135", "165", "43", "19", "162"],
["31", "159", "37", "172", "202", "16", "196", "203", "132", "252", "224", "246", "64", "186", "78", "41", "50", "0", "225", "30", "19", "124", "151", "187", "88", "232", "42", "129", "237", "151", "90", "120"],
["46","2","86","202","227","28","150","177","138","109","92","238","235","171","249","134","94","168","134","67","220","229","59","160","98","41","85","11","15","226","67","173"],
["155","80","235","144","195","28","98","58","75","57","203","87","148","251","104","142","149","245","73","25","214","71","147","153","7","205","224","41","214","13","183","176"],
["0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"],
["155","27","72","217","112","216","250","208","157","235","226","113","109","41","165","99","244","210","27","101","232","42","165","85","66","208","220","11","16","8","68","54"],
["46","2","86","202","227","28","150","177","138","109","92","238","235","171","249","134","94","168","134","67","220","229","59","160","98","41","85","11","15","226","67","173"],
["155","80","235","144","195","28","98","58","75","57","203","87","148","251","104","142","149","245","73","25","214","71","147","153","7","205","224","41","214","13","183","176"],
["0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"],
["155","27","72","217","112","216","250","208","157","235","226","113","109","41","165","99","244","210","27","101","232","42","165","85","66","208","220","11","16","8","68","54"]
],
"length": "5"
} */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment