Skip to content

Padding-kk state machine

All Keccak-related state machines are accessed through the Padding-KK state machine. It is therefore responsible for handling queries from the Main state machine. The common queries are requests for digests of messages, together with validation of these digests.

In this document, the workings of the Padding-KK SM are described. How it validates the validity of hash values, input string lengths, and input string reading to ensure that padding requirements are followed.

First, keep in mind that the Padding-KK SM’s operations are byte-based, whereas the Keccak-F SM’s actions are bit-based. This discrepancy in string formats is handled by the Padding-KK-Bit SM.

Padding and the Keccak-F SM

Although the hashing of messages is carried out by the Keccak-F SM, the padding happens in the Padding-KK SM. Messages are presented to the Padding-KK SM as byte-strings in hexadecimal form. But the Padding-KK-Bit SM ensures that these are presented as bit-strings to the Keccak-F SM.

Even though Keccak-F SM receives strings of any length as inputs, each input-message to the Keccak-F SM is first split into blocks of 1088 bits (i.e., 136 bytes), called the bit rate (or rate). If the tail-end of the splits is shorter than 136 bytes, or if the original message is shorter than 136 bytes, a specific string is appended to it in order to form a full 136-byte string. The appended bits (or bytes) are referred to as the padding.

Keccak’s first padding-rule is to append the string 1001 of the appropriate length. That is, the padding always consists of two 1’s and a string of 0’s between them.

The second padding rule: If the input-message is exactly 136 bytes long, or a multiple of 136 bytes, then a block of 136 bytes consisting of just the padding 1001, must be appended.

It is crucial to emphasise that the Polygon zkEVM follows the Keccak construction used in the Ethereum. So then the Padding-KK SM does not append any other ‘fixed’ bits to the padding, such as appending “01” as it is done in the FIPS’s SHA-3. Therefore, as far as the Keccak-F hash function is concerned, the Polygon zkEVM does not follow the FIPS.202 Standard.

Input strings and padding

Consider the strings s1,s2,,sn that need to be hashed.

Note that the bits (or bytes) of these strings cannot be simply combined and presented as one stream of bits (or bytes) as though they belong to one long string. But each string si need to be treated as an individual string, and thus must first be separately padded in line with the above-mentioned padding rules. Only thereafter, depending on the SM involved, can the bytes or the bits be fed into the relevant SM.

The idea here is to map each string to one or several blocks of 136 bytes (1088 bits), and include the proper padding at the tail-end part.

Schema of input strings allocation in Keccak

Observe that, as shown in the first string in the Figure above; Whenever the length of a certain string is a multiple of the block length of 136 bytes, a new block containing only the padding must be appended. Once this is done, the new resulting string can be provided to the Keccak-F SM for hashing.

Dealing with Main SM queries

The Padding-KK SM is in charge of validating that the padding rule is correctly performed, as well as validating each of the prescribed operations;

  1. Validate the lengths of given strings {si}. (i.e., length check),
  2. Validate the hashes of certain strings {si}. (i.e., digest check),
  3. Validate reads from 1 to 32 bytes of string {si}. (i.e., read check).

Next is to prepare the machinery that will enable the Padding-KK SM achieve these three checks.

Setting up columns for verification purposes

We now design a series of columns that will enable us to completely verify correctness of every state transitions.

  • freeIn: This register will store every byte of the padded input (as commented before), one byte per row.
  • address: This register will store an increasing sequence of integers starting from 0, changing its value at the beginning of a new string. Of course, an address completely determines the corresponding string of a certain byte.

  • connected: This register represents the connection between two blocks. If connected is 1, the actual block is in the same string with the previous block. Otherwise, the last block is in the previous string.

  • lastBlock: This register flags the last row of every block. If lastBlock is 1, the next block will start in the next row of the table.

  • rem: For each string, this register is a decreasing sequence of signed integers. Starting at the length of the current string and it keeps decreasing until the last block of the string is reached. Observe that this value can be negative since a padding may be present in the string. (rem is short for ‘remaining’.)

  • len: For each string, this register stores original length (i.e., the length of the string before padding was appended). Therefore, this register remains constant for all rows of each string. Observe that, at the first row of each string, len coincides with rem.

  • remIsZero: A computed register which is 1 whenever rem is zero, and 0 otherwise.

  • spare: The spare register is 1 just after the byte 0x01, corresponding to the appearance of padding bits. Observe that it can happen that spare is constantly 0 among a full string. This is because we can have the situation where the padding only consists of the byte 0x81.

  • lastHash: This register is actually computed from the registers spare, remIsZero and lastBlock as follows,

(Eqn.1)lastHash=lastBlock(spare+remIsZero)

This means lastHash will be 1 whenever two things hold true. First, if lastBlock=1, and secondly, if the next block to be processed is contained in the next string.

Observe that spare and remIsZero cannot simultaneously be “1” (i.e., not in the same row). Moreover, it is very important to include remIsZero in this computation because spare alone cannot help detect that padding has occurred. For instance, if pad=0x81, then spare would remain constant, continuing to record 0’s as if no padding took place.

Example: Padding check using columns

Let us illustrate this design with a table, using the columns defined above. Suppose the following strings have been padded and are ready for hashing:

s0=800000016f6c6c6568       s1=8000012f11|616c6966

where “|” indicates the end of a 136-byte block. In the below table, these 136-byte splits between blocks are indicated with a horizontal line.

freeInaddrconnectedlastBlockremlenremIsZerosparelastHash0x68000550000x65000450000x6c000350000x6c000250000x6f000150000x01000051000x00000150100x0000012950100x8000113050110x661002692690000x691002682690000x6c1001352690000x611011342690000x111101332690000x2f11012690000x0111002691000x0011012690100x801112269011

The above table illustrates how the columns can be used to record the executional trace of the Padding-KK State Machine. As it is our general approach, a strategy towards achieving verifiable computations, the next task is to describe the state transitions of the Padding-KK SM in terms of polynomials.

Description of state transitions in PIL

By capturing the relationships between and among the columns (registers defined above) in terms of equations, amounts to translating the execution of the padding into a verification code written in PIL.

  1. In order to guarantee that the value recorded in the rem register decreases until lastHash is 1 (that is, the end of the string), use the relation,

    (Eqn.2)rem(1lastHash) =(rem1)(1lastHash)
  2. How can we validate the fact that spare was constructed as expected?

    First observe that spare changes to 1 immediately after a 1 was recorded in the remIsZero register. (i.e., immediately when the padding starts, and when pad0x81.)

    Secondly, notice that after this point, spare remains 1 until (and including when) lastHash equals 1. After which, spare becomes 0.

    Hence, these behaviour can be captured as, $$ \texttt{spare}’ \mathtt{ = (spare + remIsZero)\cdot (1 - lastHash)} \tag{Eqn.3} $$

  3. Verifying correctness of the connected register requires two constraints;

    → Checking that connected is constant in each block

    (Eqn.4)connected(1lastBlock)=connected(1lastBlock)

    → Checking two specific situations,

    • When lastBlock is 1 and lastHash is 0 : in this case, the next value of connected should be 0, because of the block change but within the same string.

    • When both lastBlock and lastHash are 1 : in this case, the next value of connected should be 0, due to the string change.

    These two scenarios are verified with the following constraint,

    (Eqn.5)connectedlastBlock=(1lastHash)lastBlock
  4. The len register is constant within each string. It must therefore satisfy this relationship,

    (Eqn.6)lenfirstHash=len(1lastHash)
  5. Checking that len and rem coincide at the first state of each string, use the constraint,

    (Eqn.7)lenfirstHash=remfirstHash

    where firstHash is a committed column and it is such that firstHash=lastHash.

    In fact, firstHash is a shifted version of lastHash, which is used to ensure that, when starting a string (and therefore, firstHash=1), then len=rem.

    lastHashfirstHash       00            00       00            10       01     
  6. Let us now specify the relations that satisfy the addr register. As commented on before, addr is constant within each string. Hence, (addraddr)=0 if and only if 1lastHash0.

    The constraint for the addr register is therefore,

    (Eqn.8)(addraddr)(1lastHash)=0

    Note that going from one string to the next, the values of the addr register form an increasing sequence (increasing by steps of 1 from one string to the next).

    However, since the polynomials utilized in this scheme are all cyclic (due to evaluations on roots of unity), there is a need to ensure that the addr register resets to 0 whenever the reading returns to the first row.

    For this purpose, a register called lastBlockLatch is added. And lastBlockLatch is 1, if lastBlock is 1 and the string, that the last block belongs to, is not the last one.

    Similarly, another register called lastHashLatch is added, and it is defined such that

    (Eqn.9)lastHashLatch=lastBlockLatch(spare+remIsZero)

    See below table for a comparison between the latch and non-latch registers.

    lastBlocklastBlockLatchlastHashlastHashLatch       0000            1100       0000            1111       0000                 1010
  7. In order to grapple with the increasing but cyclic sequence of addr, the following constraint is used,

    (Eqn.10)(addr1addr)lastHashLatch=0
  8. Now, checking whether remIsZero is 1 if and only if rem is 0, is done reversely by first committing the column remInv, the inverse of rem, and computing remIsZero as: $$ \mathtt{remIsZero = 1 − rem · remInv} \tag{Eqn.11} $$

    And then, as usual, check the relation, remIsZerorem=0.

  9. Next is the aFreeIn register which stores the input byte if and only if the current row does not corresponding to the padding. aFreeIn is computed from the remIsZero, spare and lastHash registers. This ensures loading the padding bytes at their correct positions.

    In fact, this register will be used in the Plookup of the next state machine.

    Observe that aFreeIn can be computed as

    (Eqn.12)aFreeIn=(1(remIsZero+spare))freeIn+remIsZero+lastHash0x80

    Let us carefully analyze this equation:

    • If remIsZero, spare and lastHash are all 0, then aFreeIn equals freeIn. As mentioned above, at the non-padding rows, we just store freeIn.
    • If remIsZero is 1, spare is 0 and lastHash is 0, then aFreeIn equals 0x01, which is the first byte of the padding.
    • If remIsZero is 0, spare is 1 and lastHash is 0, then aFreeIn equals 0x00, which are the intermediate bytes of the padding.
    • If remIsZero is 0, spare is 1 and lastHash is 1, then aFreeIn equals 0x80, which is the last byte of the padding.
    • Lastly, we should consider the special case where the padding is only the byte 0x81. In this case, spare is 0 at the last row meanwhile lastHash and remIsZero are both 1. Therefore, aFreeIn equals 0x81, as we wanted.

    See below table for all the above cases when computing aFreeIn.

remIsZerosparelastHashaFreeIn       000freeIn       1000x01       0100x00       0110x80       1010x81

Hash output check

We have thus far only dealt with correct inputs of the padding. Now, we will introduce several columns to deal with the hash itself.

Since KECCAK-256’s output is 256 bits long, we use eight (8) registers each of 32 bits to store the result of the hash function. Denote these registers by {hashi | i{0,1,2,,7}}.

As columns, these {hashi} registers will remain constant within a single string, because they represent the hash of a given string. The following constraints are therefore added,

(Eqn.13)(hashhash)·(1lastHash)=0

A combination of other KECCAK-related state machines will be used to verify correctness of the output hash. The reason for this is that, for compatibility with the KECCAK-256 hash function, we first need to describe all inputs in terms of bits.

Length and read check operations

In this section we give a description of the design that will allow us to verify the lengths of input strings and the read operations.

Length check

The length check is almost trivial because the len register has already been introduced.

Suppose one is given a length l and an address a. And the aim is to check if the string corresponding to the address a has length l.

The strategy is to use Plookup to verify that the given table contains a row with len=l and an address addr=a in the last row of the string (i.e., when lastHash=1). That is to say, the table of columns; len, addr and lastHash; is as displayed in the table provided below.

lastHash addr  len        00            10       01            11       0            1       0al            0al       1al       0a+1     

The PIL code for this Plookup looks like this:

hashKLen{ addr, op0 }inPaddingKK.lastHashLatch{ PaddingKK.addr, PaddingKK.len  }

where hashKLen flags whenever the operation is checked and the length is stored in op0.

Read check

Checking reads requires more columns to be introduced. Recall that, given given three parameters; the address a of a string, the position p of the starting byte of the string, and l the total number of bytes that we want to read; the intention is to read the bytes of the string from the first to thirty second.

It takes 8 registers, each of 32-bits, to store the output of the read operation. Let us denote these registers by:

{ crVi | i{0,1,2,,7}}

Further 8 factor polynomials {crFj| j{0,1,2,,7}} are needed so as to correctly place the input bytes with respect to their right power of 2.

Before looking into the roles of the {crVi} registers and {crFi} factor polynomials, three more columns are necessary for describing the verification of the read operations. These registers are; crLen, crOffset and crLatch; and are defined as follows.

  • crLen: A register that specifies the number bytes to be read. It is a number register, containing numbers between 1 and 32, and it remains constant in each of the readings we want to perform.

  • crOffset: A decreasing sequence of values, starting from the length value of the read minus 1 (i.e., crLen1 ) and ends at 0, for each read. This is important for positioning each byte at the correct power of 2.

  • crLatch: This is a computed column, using the same inverse technique as before. It records instances when the crOffset register is 0. First, the register crOffsetInv is committed, allowing crLatch to be expressed as $$ \mathtt{crLatch = 1 − crOffset \cdot crOffsetInv} \tag{Eqn.14} $$ which satisfies the constraint, crOffsetInvcrLatch=0.

Example: Read check using columns

Suppose we want to read the first 10 bytes of the address 6, and once finished, read the next two bytes of the same address thereafter. Here is the correct way of constructing the polynomials; crLen, crOffset and crLatch. Consider the string, s=0x...7355ff00111a6e6e1f02ef10.

aFreeIn addr  crLen crOffsetcrLatch   0x1061090   0xef61080   0x0261070   0x1f61060   0x6e61050   0x6e61040   0x1a61030   0x1161020   0x0061010   0xff61001   0x556210   0x736201    

Note that the horizontal line is used to separate every 8 bytes, which are the exact number of bytes stored in a crVi register. We will later on see why this is important.

Several observations

Firstly, note that crLatch is 1 if and only if crOffset is 0.

Secondly, if we want to express the two read values as an array, the output will be $$ \mathtt{[0xff00111a6e6e1f02ef10, 0x7355]} $$ where the first element of the array has 10 bytes, whilst the second has 2 bytes. This coincides with each of the crLen defined values.

Thirdly, observe the relationships that these registers; crLen, crOffset and crLatch ; have to satisfy.

(a) The crOffset register decreases in every read, so we can express this as $$ \mathtt{crOffset′ \cdot (1 − crLatch) = (crOffset − 1) \cdot (1 − crLatch)} \tag{Eqn.15} $$ (b) The crLen register remains constant during each read operation, hence the following constraint applies, $$ \mathtt{crLen′ \cdot (1 − crLatch) = crLen \cdot (1 − crLatch)} \tag{Eqn.16} $$ (c) The first crOffset of every read is crLen − 1. Hence, we need to specify the following relationship $$ \mathtt{crLatch \cdot crOffset’ = crLatch \cdot (crLen′ − 1)} \tag{Eqn.17} $$ (d) In addition, we need to ensure that the address resets immediately after a string ends. Otherwise, we will read from a different string. For this reason, we need to introduce the following constraint $$ \mathtt{(1 − crLatch) \cdot lastHash} \tag{Eqn.18} $$ which, when combined with the previous constraints enforces the desired results.

The registers {crVi} and polynomial factors {crFi}

Let us now turn to the registers crVi and polynomial factors crFi.

Consider the previous example, where the first element of the ‘output’ array is $$ \mathtt{0xff00111a6e6e1f02ef10} $$ Obviously, this element does not fit in a 32-bit register. So it needs to be split over three registers; crV0, crV1 and crV2; respectively as, $$ \mathtt{0x1f02ef10, 0x111a6e6e} \text{ and } \mathtt{0xff00} $$ Moreover, each byte has a corresponding weight (28)j, for some j{0,1,2,3}, such that,

0x1f02ef10=0x1020+0xef28+0x02216+0x1f2240x111a6e6e=0x6e20+0x6e28+0x1a216+0x112240xff00=0x0020+0xff28 

Henceforth, we should reflect this in our state machine using the columns crFi and crVi for our previous example.

aFreeIncrLatchcrV0crV1crV2crF0crF1crF2   0x1000x100x000x00100   0xef00xef100x000x002800   0x0200x02ef100x000x0021600   0x1f00x1f02ef100x000x0022400   0x6e00x1f02ef100x6e0x00010   0x6e00x1f02ef100x6e6e0x000280   0x1a00x1f02ef100x1a6e6e0x0002160   0x1100x1f02ef100x111a6e6e0x0002240   0x0000x1f02ef100x111a6e6e0x00001   0xff10x1f02ef100x111a6e6e0xff000028   0x5500x550x000x00100   0x7310x73550x000x002800    

Observe that registers {crVi} are actually cumulative up to the register’s maximal capacity of 4 bytes is reached, in which case the next reads are stored in the next crVi+1 register.

The registers {crFi} are responsible for correct positioning of each byte with respect to powers (28)j, for j{0,1,2,3}, providing the decomposition stated above.

Lastly, observe that the rows corresponding to crLatch=1, store the complete result of the read in the registers {crVi}. These are the rows where validity of the read operation is checked via a Plookup.

Constraints pertaining to {crVi} and {crFi}

The idea here is to compute a column crVCi defined by,

(Eqn.19)crVCi=crVi+crFiaFreeIn

and then verify that the next state crVi coincides with crVCi whenever crLatch is not equal to 1. This will confirm that the {crVi} registers are not only sequentially read, but are also correctly computed from the previous states. Hence, the following constraint needs to be added;

(Eqn.20)crV=crVC(1crLatch)

So far so good.

Now observe that the tuple (crOffset,crF0,crF1,...,crF7) is not totally arbitrary. In fact, it needs to be checked whether crOffset{1,,32} and crFi{1,28,216,224}. This is done via a Plookup. That is, checking if the row corresponding to crLatch=1 is contained among the previously computed table of constants having all possible combinations of the previous columns.

{    crOffset,crF0,crF1,crF2,crF3,crF4,crF5,crF6,crF7}  in{  k_crOffset,k_crF0,k_crF1,k_crF2,k_crF3,k_crF4,k_crF5,k_crF6,k_crF7};  

Read operation and Main state machine

There are a few subtle details concerning the connection between the Read operation and the Main SM to be taken into account.

For instance, in order to avoid problems, reading from the last (probably smaller) block is not allowed. A constant column r8Valid is therefore created specifically for checking if a read is done at the last block. So, r8Valid will be 1 if and only if the current state is not at the last block. Hence, the Plookup in the Main SM’s PIL code (main.pil code; lines 663 to 676):

hashK+hashK1 {addr,HASHPOS,D0 * hashK + hashK1,op0, op1, op2, op3,op4, op5, op6, op7} in PaddingKK.crLatchPaddingKK.r8Valid {PaddingKK.addr,PaddingKK.lenPaddingKK.remPaddingKK.crLen+1,PaddingKK.crLen,PaddingKK.crV0C, PaddingKK.crV1C, PaddingKK.crV2C, PaddingKK.crV3C,PaddingKK.crV4C, PaddingKK.crV5C, PaddingKK.crV6C, PaddingKK.crV7C};  

Please note that these quoted code lines will differ with different versions. They should only be taken as examples for a particular code version.

Note that HASHPOS is marking the position in the string of the byte where the reading starts. The Plookup shown above, maps the committed polynomial HASHPOS in the Main SM to the linear combination of the committed polynomials; len, rem and crLen; of the Padding-KK SM as,

(Eqn.20)lenremcrLen+1

Let us make use of the following example to clearly see why this linear combination works.

Example: Main SM and Padding-KK connection

Suppose we want to read bytes 2, 3 and 4 of the string 0x7766554433221100. The table of the reading, reflecting only the relevant columns, is as follows.

aFreeInlenremcrLenlen - rem - crLen + 1crV1line 00x0088010x00line 10x1187020x00line 20x2286300x22line 30x3385310x3322line 40x4484320x443322line 50x5583060x00line 60x6682070x00line 70x7781080x00

Observe that, among the rows with crLen=3 (the ones where the reading happens), only the row in line 6 contains the complete read-result, 0x443322, in the crVi column. That is, although HASHPOS = 2, meaning the reading starts with byte 2 which is in line 2, the reading is only complete in line 6. And that’s the exact row where the linear combination works out to be

(lenremcrLen+1)=2=HASHPOS

Last update: January 17, 2024
Authors: avenbreaks