Implement Inner Product Using Coefficient Encoding

I am trying to compute the sum of squares of plaintext coefficients using coefficient encoding in BFV without Key Switching.
The idea is to exploit the identity:

f(X)f(X^{-1}) = a_0^2 + a_1^2 + \cdots + a_{n-1}^2 + \ldots

so that the constant term gives the inner product of the coefficient vector.

To compute under encryption, I performed the following steps:

  1. Encrypt a coefficient-packed plaintext f(X) (ct = (c0(X), c1(X)))
  2. Generate f(X^-1) under encryption using automorphism with index 2N - 1.
  3. Combine the ciphertexts using the decryption equation:
m(X) = c_0(X) + c_1(X)s(X)

and its automorphic counterpart:

m(X^{-1}) = c_0(X^{-1}) + c_1(X^{-1})s(X^{-1})

Then the product expands as:

\begin{aligned} m(X)m(X^{-1}) &= \big(c_0(X) + c_1(X)s(X)\big) \big(c_0(X^{-1}) + c_1(X^{-1})s(X^{-1})\big) \\ &= c_0(X)c_0(X^{-1}) + c_0(X)c_1(X^{-1})s(X^{-1}) \\ &\quad + c_0(X^{-1})c_1(X)s(X) + c_1(X)c_1(X^{-1})s(X)s(X^{-1}) \end{aligned}
  1. Extract the constant term from the result.
#include "openfhe.h"

using namespace lbcrypto;


PrivateKey<DCRTPoly> MyAutomorphismKeyGen(const PrivateKey<DCRTPoly> privateKey) {
    const auto cc = privateKey->GetCryptoContext();
    const auto& s = privateKey->GetPrivateElement();
    auto algo = cc->GetScheme();

    uint32_t N = s.GetRingDimension();

    auto privateKeyPermuted = std::make_shared<PrivateKeyImpl<DCRTPoly>>(cc);

    uint32_t index = 2*N - 1;
    std::vector<uint32_t> vec(N);
    PrecomputeAutoMap(N, index, &vec);
    DCRTPoly sPermuted = s.AutomorphismTransform(index, vec);

    privateKeyPermuted->SetPrivateElement(std::move(sPermuted));
    privateKeyPermuted->SetKeyTag(privateKey->GetKeyTag());

    return privateKeyPermuted;
}

Ciphertext<DCRTPoly> Myautomorphism(Ciphertext<DCRTPoly> ct, CryptoContext<DCRTPoly> cc){
    uint32_t N = ct->GetElements()[0].GetRingDimension();
    std::vector<uint32_t> vec(N);
    PrecomputeAutoMap(N, 2*N - 1, &vec);

    auto result = ct->Clone();
    //RelinearizeCore(result, evalKeyMap.at(i));
    auto& rcv = result->GetElements();
    rcv[0]    = rcv[0].AutomorphismTransform(2*N - 1, vec);
    rcv[1]    = rcv[1].AutomorphismTransform(2*N - 1, vec);
    return result;
}
int main() {
    
    CCParams<CryptoContextBFVRNS> parameters;
    parameters.SetPlaintextModulus(65537);
    parameters.SetMultiplicativeDepth(2);

    CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters);
    
    cc->Enable(PKE);
    cc->Enable(KEYSWITCH);
    cc->Enable(LEVELEDSHE);

    KeyPair<DCRTPoly> keypair = cc->KeyGen();

    PrivateKey<DCRTPoly> permuted = MyAutomorphismKeyGen(keypair.secretKey);

    //make s(x)*s(1/x)
    auto s = keypair.secretKey->GetPrivateElement();
    auto s_inv = permuted->GetPrivateElement();

    std::vector<int64_t> vectorOfInts1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

    Plaintext pt = cc->MakeCoefPackedPlaintext(vectorOfInts1);

    auto ct = cc->Encrypt(keypair.publicKey, pt);
    
    auto ct1 = Myautomorphism(ct, cc);

    auto rct = ct->GetElements();
    auto rct1 = ct1->GetElements();
    
    DCRTPoly m = rct[0]*rct1[0] + rct[0]*rct1[1]*s_inv + rct1[0]*rct[1]*s + rct[1]*rct1[1]*s*s_inv;

    uint64_t t = 65537;

    auto coeff = m.GetElementAtIndex(0).GetValues()[0];
    uint64_t constant = coeff.ConvertToInt() % t;
    std::cout << "constant term mod " << t << " = " << constant << std::endl;
}

The constant term I obtain is not equal to sum of squares.
I would really appreciate it if the OpenFHE team could confirm whether my reasoning and usage of AutomorphismTransform are conceptually correct for coefficient encoding.

I saw a similar (probably simpler) approach in the literature: https://onlinelibrary.wiley.com/doi/epdf/10.1002/sec.1164 You might want to look at it.

Automorphism for a coefficient-encoded plaintext works differently from the slot-encoded plaintext. The automorphism operation is designed to natively support the slot encoding (and use isomorphism properties). My suggestion is to print out the result after the automorphism and decrypt it using s_inv to see what you actually get, and debug it from there.

Hi β€” thanks for the pointer and for the quick reply.

A short follow-up describing where I am and what I need help with:

I implemented MyAutomorphismKeyGen and MyAutomorphism to produce s(1/x) and the transformed ciphertext (c0(1/x), c1(1/x)).

Decrypting that transformed ciphertext with s(1/x) yields the expected m(1/x) β€” so the automorphism and key generation appear correct.

The issue: when I form the product

(c_0(π‘₯)+𝑐_1(π‘₯)𝑠(x))β‹…(𝑐_0(1/π‘₯)+𝑐_1(1/π‘₯)𝑠(1/x))

and look at the constant term, the result is not the expected

βˆ‘a_i^2

Everything else (individual automorphism decryption, s_inv correctness) looks fine.

As a follow-up, here’s my decryption routine.

DCRTPoly PKERNS::MyinnerproductDecryptCore(const std::vector<DCRTPoly>& cv1, const std::vector<DCRTPoly>& cv2,
    const PrivateKey<DCRTPoly> privateKey1, const PrivateKey<DCRTPoly> privateKey2) const {

    const DCRTPoly& s1 = privateKey1->GetPrivateElement();
    const DCRTPoly& s2 = privateKey2->GetPrivateElement();

    size_t diffQl = s1.GetParams()->GetParams().size() - cv1[0].GetParams()->GetParams().size();

    auto scopy1(s1);
    auto scopy2(s2);
    scopy1.DropLastElements(diffQl);
    scopy2.DropLastElements(diffQl);

    DCRTPoly sPower1(scopy1);
    DCRTPoly sPower2(scopy2);

    DCRTPoly c11 = cv1[0];
    DCRTPoly c12 = cv1[1];
    DCRTPoly c21 = cv2[0];
    DCRTPoly c22 = cv2[1];

    c11.SetFormat(Format::EVALUATION);
    c12.SetFormat(Format::EVALUATION);
    c21.SetFormat(Format::EVALUATION);
    c22.SetFormat(Format::EVALUATION);

    // rct[0]*rct1[0] + rct[0]*rct1[1]*s_inv + rct1[0]*rct[1]*s + rct[1]*rct1[1]*s*s_inv
    DCRTPoly b1(c11);
    DCRTPoly b2(c21);
    b1 += sPower1 * c12;
    b2 += sPower2 * c22;

    b1 *= b2;

    sPower1 *= scopy1;
    sPower2 *= scopy2;

    return b1;
}

What I want to know (please):

  1. Is my suspicion correct that I need to scale-down (divide by Ξ”) after forming the product to restore the correct plaintext scale?

  2. If so, what is the recommended / canonical way to do this inside OpenFHE (BFV, RNS implementation) before final coefficient extraction

  3. Role of sPower *= scopy:
    I noticed that the DCRTPoly PKERNS::DecryptCore Function use this operation:

sPower *= scopy;

Could you explain the mathematical or algorithmic purpose of this operation?

In homomorphic BFV multiplication, the first step is to extend the basis of each ciphertext from Q to PQ = Q^2 + \epsilon so that the tensor product could be performed without modular reduction. Then the result is scaled down (multiplied by t/Q). This procedure is described in 2018/117 and further improved in 2021/204. You would need to do something similar in your case. In your current code, you are not extending from Q and are basically getting nonsense because of overflows. You should be able to use the existing BFV multiplication tensor product functionality (EvalMultNoRelin) for this purpose.

I am going to switch the category to FHE questions as this question does not directly relate to any existing OpenFHE functionality. It is effectively a research question.

thanks for advise. I solved this problem! thank you very much.