I think I figure it out, let me know if I do it wrongly
BigInteger primeModulus("113"); // 65537
uint32_t cycloOrder = 8;
uint32_t n = cycloOrder / 2;
uint64_t m = n << 1;
NativeInteger modulusQ(primeModulus);
NativeInteger rootOfUnity = RootOfUnity(m, modulusQ);
DiscreteUniformGeneratorImpl<NativeVector> dug;
NativeVector x = dug.GenerateVector(n, modulusQ);
NativeVector X(n);
ChineseRemainderTransformFTT<NativeVector> crtFTT;
crtFTT.PreCompute(rootOfUnity, m, modulusQ);
std::cout << "x before INTT: " << x << std::endl;
crtFTT.ForwardTransformToBitReverse(x, rootOfUnity, m, &X);
std::cout << "X after INTT: " << X << std::endl;
crtFTT.InverseTransformFromBitReverse(X, rootOfUnity, m, &x);
std::cout << "x after NTT: " << x << std::endl;
Might I ask how shall I understand the value being printed out from here? Should it be the coefficients of the ciphertext?
CycloOrder: 8
rootOfUnity: 18
x after INTT: [1 2 4 1] modulus: 113
X after INTT: [46 62 104 18] modulus: 113
x after INTT: [1 2 4 1] modulus: 113
I get above messages, where 18 is the 2n-th root of unity instead of n-th root of unity for a degree N=4. And the naive NTT following the algorithm (Discrete Fourier transform over a ring - Wikipedia) gives me the result of [8, 95, 2, 12]. Here come the Python code of implementing it. Might I seek suggestions on why am I obtaining a different result?
import math
def bit_reverse_indices(n):
"""
Return a list of indices in bit-reversed order for an array of length n (n must be a power of 2).
"""
bits = n.bit_length() - 1
result = []
for i in range(n):
rev = 0
x = i
for _ in range(bits):
rev = (rev << 1) | (x & 1)
x >>= 1
result.append(rev)
return result
def bit_reverse_permutation(arr):
"""
Reorder the array 'arr' according to the bit-reversal permutation.
"""
n = len(arr)
indices = bit_reverse_indices(n)
return [arr[i] for i in indices]
def ntt_bit_reverse(x, q, omega):
"""
Compute the forward Number Theoretic Transform (NTT) of the input vector x over GF(q)
using the iterative Cooley–Tukey algorithm with bit-reversal reordering.
The NTT is defined as:
X[k] = sum_{j=0}^{N-1} x[j] * omega^(j*k) mod q,
where omega is a primitive N-th root of unity in GF(q).
Parameters:
x : list of integers, length must be a power of 2.
q : prime modulus.
omega : a primitive N-th root of unity in GF(q), where N = len(x).
Returns:
A list of integers representing the NTT of x in natural (standard) order.
"""
n = len(x)
# Step 1: Reorder the input into bit-reversed order.
a = bit_reverse_permutation(x[:]) # copy and reorder
# Step 2: Iteratively perform butterfly updates.
# The algorithm runs in log2(n) stages.
m = 2
while m <= n:
# Compute twiddle factor for this stage:
# In each stage, we need a factor: w_m = omega^(n/m) mod q.
w_m = pow(omega, n // m, q)
# Process blocks of length m.
for k in range(0, n, m):
w = 1
for j in range(m // 2):
t = (w * a[k + j + m//2]) % q
u = a[k + j]
a[k + j] = (u + t) % q
a[k + j + m//2] = (u - t) % q
w = (w * w_m) % q
m *= 2
return a
# Example usage:
if __name__ == '__main__':
# Parameters:
# Let q = 17, N = 8, and omega = 2.
# 2 is a primitive 8-th root of unity modulo 17 since 2^8 = 256 ≡ 1 (mod 17) and no smaller power yields 1.
q = 113
N = 4
omega = 18 ** 2 % q # 98
# Input vector x.
x = [1, 2, 4, 1]
# Compute the NTT using the bit-reverse based iterative algorithm.
X = ntt_bit_reverse(x, q, omega)
print("NTT of x:", X)
Aha I found that I’m implementing the vanilla NTT, which is not for RLWE. The problem goes away after supporting negacyclic NTT via (1) 2n-primitive root and (2) twist input before sending it to NTT and twisted it back after getting the result.