The criteria is:
- Low multiplicative complexity
- Implementable in Solidity / EVM with low gas cost
- Work as a hash function
- Can be used to construct a Merkle tree
Candidates from authenticated MACs:
Candidates from universal hashing:
Specifically for the Merkle tree, where all information is public, all that we're concerned about is collision resistance and malleability. Specifically in this case the hash127 algorithm can be used with a fixed key (r
) per tree level. Where s = (r^{n+1} + (r^n * m_0) + (r^{n-1} * m_1) + ... + (r * m_{n-1})) mod p
, m
is split into 32 bit chunks and m != 0
. The values of r^n
etc. can be pre-computed.
Concerning malleability, where each message chunk is the size of a field element we can construct an input which resets the intermediate sum into a known state, the smaller the chunk size the less malleable it becomes at the cost of greater multiplicative complexity. For example with field elements as message input we could just find something that when multiplied by r
is the difference between the current sum and our target value to produce a collision, and the more message blocks you have control over the easier this gets.
However, splitting into individual bits increases the complexity of the circuit as we need to verify the bitness of every message input to prevent malleability. If the messages are 256 bits each and the merkle tree is 29 deep, that requires 7424 constraints to verify all input bits. Where we compress two messages into 1 field element using 32 bit blocks it requires an additional 16 constraints per level (464 for 29 levels). Then an additional 464 constraints to compute the polynomial for the 29 levels, which puts the total at 8355 constraints.
There is another approach for LongsightF where, when used as a cipher, it may still be usable as the merkle path authenticator there the number of rounds is less significant. In this case we know that for any given result we can walk backwards to find any number of colliding inputs with control over both input parameters, essentially when used as a merkle path authenticator its construction is similar to a sponge construction in duplex mode.
However, the security of the whole authentication path comes down to finding a single colliding input which is essentially the first item in the Merkle tree path, which means the complexity isn't improved by using the merkle-damgård construct, that is finding appropriate inputs for level 28 is as linear after running level 29 in reverse. A good reference for different constructions is: https://www.emsec.rub.de/media/crypto/attachments/files/2011/03/bartkewitz.pdf
The clearest example I can think of which demonstrates this is the collision resistance of LongsightL
where the key can be chosen - if you can choose the keys for each round it's very malleable. However, if the round keys are derived via squaring of a single input then you've essentially eliminated the malleability.
e.g.
def DoublevisionH_a(k, m, r, p):
x = k
for _ in range(0, r):
y = (m + x) % p
m = (y * y) % p
x = (x * k) % p
return m
Even with only 2 values of m
this intuitively becomes an intractable problem, where for N rounds the message is multiplied by the square of the key, this makes it very similar to the polynomial used by hash127 and Poly1305+AES with the exception that the message blocks and round constants are derived from two initial values.
The problem we have is the parity of quadratic residue where both m
and k
are squares after the first round, for example there is a linear route backwards and we know the round function isn't bijective. Interesting references:
When used with the curve order of altBN, as used by the field of the zkSNARK circuit, we can ensure that for any fixed k
and any 0 < m < p
the result will be a bijection as exponentiating by 5 is also a bijection:
def DoublevisionH_b(m, p, r, k):
x = k
for _ in range(0, r):
y = (m + x) % p
m = powmod(y, 5, p)
x = x * k
return m
But, we can't generalise this for any k
and any m
as there will be overlap while still satisfying the pigeon hole principal. The best thing we can do is to use two bijective sequences, which in turn are also bijective where either k
or m
are fixed.
def DoublevisionH_c(m, p, r, k):
x = k
for _ in range(0, r):
y = (m + x) % p
m = powmod(y, 5, p)
x = powmod(x, 5, p)
return m
The problem with this though, is that by controlling either k
or m
you can force y
to be zero, which reduces the complexity to finding an initial k
than when squared/exponentiated r
times results in the desired m
.
Another problem is that m
and k
can be switched and the result is the same, when used in a Merkle tree this is bad as we need to be strictly order-enforcing, whereas when x
is squared at each iteration the order is enforced, likewise if k
is exponentiated in a bijective fashion m
can be squared and the result is still a bijection.
The y
step can be either additive or multiplicative.
However, after further testing the problem with either of these is that when m
is fixed and k
is variable it isn't a bijection, the function can be modified to satisfy this requirement, but then only the last value of y
is used which removes a layer of complexity.
def DoublevisionH_d(m, p, r, k):
for _ in range(0, r):
y = (m + x) % p
m = powmod(m, 5, p)
k = powmod(k, 5, p)
return y
Adding y
to either k
or m
retains the quality of being a bijection where the same argument that's fixed is the one which y
is added to. e.g. for all k \in Z_p
and k=(k+y)^5
where m
is constant and visa versa.
def DoublevisionH_e(m, k, p):
y = 0
r = 4
for _ in range(0, r):
y = (m + k) % p
m = powmod(m, 5, p)
k = powmod((k + y) % p, 5, p)
return y
In my pigeonhole surjection test, which measures the distribution of pigeons in holes for all N
and M
for H(N,M)
e.g. for N in range(0,p): for M in range(0,p): H(N,M)
, the standard deviation for LongsightF hovers at around 2 and the variance at 4. Whereas with DoublevisionH_e
the standard deviation hovers at around 1 and variance at 1. If I add y
to both k
and m
before exponentiating them we get similar standard deviation and variance as LongsightF.
So we have a measurable quality which indicates a bijection that the variance is ~1 and stddev is ~1, whereas without a bijection the stddev is ~2 and variance ~4 which indicates the bijective flavour is more consistently distributed, which indicates less randomness.
But, is more random distribution a good quality? Is there a relation between the bijective nature when one parameter is constant, versus something which isn't a bijection. My gut feel is that, with a large enough |p|
the additional complexity of having both m
and k
be x=(x+y)^5
computationally binds both parameters to the value from the previous round. It doesn't solve the "first round y is zero" problem though, but I think that's unsolvable in that you can always algebraically negate the first round with control over one parameter - but that doesn't give you control over the other one which is the crux of the matter.
Which leaves us with one interesting candidate:
def DoublevisionH_f(m, k, p, r):
y = 0
e = 5
for _ in range(0, r):
y = (m + k) % p
m = powmod((m + y) % p, e, p)
k = powmod((k + y) % p, e, p)
return y
Where the exponent 5
is replaced with whatever is a bijection for that specific field. However, it still suffers the problem of the argument order being reversible due to the commutative nature of the y
step, and for a Merkle tree it's absolutely necessary to have ordering.
For the ordering property we can use two sequences of round constants for both m
and k
, which becomes:
def DoublevisionH_g(m, k, p, r, C):
y = 0
e = 5
for i in range(0, r):
y = (m * k) % p
m = powmod((m + y + C[(2*i)]) % p, e, p)
k = powmod((k + y + C[(2*i)+1]) % p, e, p)
return y
This ensures that, for round 0, when k
and m
are equal, y
will also be equal, but for round 1, the sequences m
and k
will have diverged and will be dependent upon their initial value.
Furthermore, by using multiplication in the step for y
the degree of the polynomial increases at each round, such that deg(f(x)g(x)) = deg(f(x)) + deg(g(x))
we can show that:
- Round 1
y = m * k
deg(y) = 0
m = (m + y + C_?)^5
deg(m) = 5
k = k + y + C_?
deg(k) = 5
- Round 2
y = m * k
deg(y) = deg(m) + deg(k) = 10
m = (m + y + C_?)^5
deg(m) = 10
(as the degree of y
is 10)
k = k + y + C_?
deg(k) = 10
- Round 3
y = m * k
deg(y) = deg(m) + deg(k) = 20
- ...
- ...
As such, the degree doubles at every round, such that after 127 rounds the degree exceeds 2^128, after 30 rounds the degree exceeds 2^31 etc. However, does that mean a collision could be found in 2^r
probability using higher order differential cryptanalysis? And furthermore, is my statement about the degree of the polynomial correct, or is it limited to 5 as that's the outside exponent of the k
and m
equations?
An alternative which pushes the degree further up into the statement is:
def DoublevisionH_h(m, k, p, r, C):
y = 0
e = 5
for i in range(0, r):
y = (m * k) % p
m = (powmod((m + C[(2*i)]) % p, e, p) + y) % p
k = (powmod((k + C[(2*i)+1]) % p, e, p) + y) % p
return y
Which makes it:
- Round 0
y = (m * k)
m = (m + C_?)^5 + (m * k)
k = (k + C_?)^5 + (m * k)
- Round 1
y = ((m+C_?)^5 * (k+C_?)^5)
deg(y) = 10
m = (m + C_?)^5 + y
deg(m) == 10
?
- etc.
- etc.
In that case, if you were to multiply by y
in stages m
and k
surely that would increase the rate of the degree at every iteration, at the cost of multiplicative complexity, for example:
def DoublevisionH_i(m, k, p, r, C):
y = 0
e = 5
for i in range(0, r):
y = (m * k) % p
m = (powmod((m + C[(2*i)]) % p, e, p) * y) % p
k = (powmod((k + C[(2*i)+1]) % p, e, p) * y) % p
return y
Where:
- Round 0 = degree 0
- Round 1 = degree 10
- Round 2 = degree 30
- Round 3 = degree 70
- etc.
However, we've diverged far away from my original proof of the bijective property, and gotten into very speculative musings of the degree of polynomials and its relation to security under higher order differential analysis.
The following paper by Xuejia Lai researches on the security of multivariate hash functions: [1] https://eprint.iacr.org/2008/350.pdf - which references MQ-HASH - [2] https://pdfs.semanticscholar.org/a258/904c3e021df8c2de621b7dfa4dc504c8f3b2.pdf - 'On Building Hash Functions From Multivariate Quadratic Equations - Billet, Robshaw & Peyrin' which is further supplemented by 'Interpreting Hash Function Security Proofs' - [3] https://infoscience.epfl.ch/record/172008/files/Juraj-Provsec2010.pdf
In [1] Lai states the degree of MQ-HASH is four... where it's necessary to compute the d
th derivative for 2^d
inputs, where d
is the degree of the multivariate function, if the derivative is a constant we can distinguish the function sampled from a uniformly distributed function, although soemtiems its difficult to translate from mathlish, but it implies that a higher order differential attack requires computation equal to 2^d
where d
is the degree of the polynomial.
The reason I wanted to rely strongly on the property of a bijection for either k
or m
is to reduce the sparseness of the polynomial surjection for both k
and m
. In § 4.1 of [1] Lai describes the probability of trivial collisions, however this is represented over GF(2)
rather than GF(p)
.
.... brain slowly melts I think a better way of analysing this would be via calculus and its relation to quadratic residue over a field.
Either way, we can break this down into the statement:
x[i] = (x[i-1] + C[i])^5 * x[i-1]
Comparative to the MiMC round function:
x[i] = (x[i-1] + C[i])^5 + x[i-2]
Where the MiMC function uses two variables, denoted by x[i-1]
and x[i-2]
, our round function when truncated to a single variable doubles the polynomial degree. When expanded to account for two variables it becomes:
z[i] = x[i-1] * y[i-1]
x[i] = (x[i-1] + Cx[i])^5 * z[i]
y[i] = (y[i-1] + Cy[i])^5 * z[i]
So, to summarise, we now have a compression function which:
- takes two arbitrary field elements as inputs
- the input parameters are ordered after at least 1 round
- the following are intractable problems:
- finding the inverse from the result
- finding a target collision using either
k
or m
where the other element is a constant
I think this is good enough to be used as a compression function to form a Merkle tree.