Can FHE be used for comparing 2 ciphertexts?

Hi! I’m curious about using homomorphic encryption to compare two encrypted values, even if they were encrypted by different people. I’m new to these concepts, so any insights would be really helpful!

Hi @Pavanranganath, as @Caesar kindly pointed out to me in a related question (see Ciphertexts equality testing in CKKS)

However if the ciphertexts are encrypted with the same public key, there are some comparison functions available. The result will be encrypted though.

I hope this helps.

I’m attempting to convert ciphertext encrypted under the same public key from CKKS to TFHE, conduct comparisons using circuits, and then return the highest ciphertext back in CKKS format. However, I’m encountering an issue with performing gate operations between the output of EvalSign and EvalCKKStoFHEW because their moduli are different. Can someone please provide insight into this problem and help me resolve it?

def compareStmt(input1, input2):
    # Step 1: Setup CryptoContext for CKKS
    scTech = FIXEDAUTO
    multDepth = 17
    if scTech == FLEXIBLEAUTOEXT:
        multDepth += 1

    firstModSize = 60
    scaleModSize = 50
    ringDim = 8192
    sl = HEStd_NotSet
    slBin = TOY
    logQ_ccLWE = 23
    slots = 16  # sparsely-packed
    batchSize = slots

    parameters = CCParamsCKKSRNS()
    parameters.SetMultiplicativeDepth(multDepth)
    parameters.SetFirstModSize(firstModSize)
    parameters.SetScalingModSize(scaleModSize)
    parameters.SetScalingTechnique(scTech)
    parameters.SetSecurityLevel(sl)
    parameters.SetRingDim(ringDim)
    parameters.SetBatchSize(batchSize)

    cc = GenCryptoContext(parameters)

    # Enable the features that you wish to use
    cc.Enable(PKE)
    cc.Enable(KEYSWITCH)
    cc.Enable(LEVELEDSHE)
    cc.Enable(ADVANCEDSHE)
    cc.Enable(SCHEMESWITCH)

    print(
        f"CKKS scheme is using ring dimension {cc.GetRingDimension()},\n number of slots {slots}, and suports a multiplicative depth of {multDepth}\n"
    )

    # Generate encryption keys.
    keys = cc.KeyGen()

    # Step 2: Prepare the FHEW cryptocontext and keys for FHEW and scheme switching
    arbFunc = False
    FHEWparams = cc.EvalSchemeSwitchingSetup(
        sl, slBin, arbFunc, logQ_ccLWE, False, slots
    )

    ccLWE = FHEWparams[0]
    privateKeyFHEW = FHEWparams[1]

    cc.EvalSchemeSwitchingKeyGen(keys, privateKeyFHEW)

    # Generate bootstrapping key for EvalFloor
    ccLWE.BTKeyGen(privateKeyFHEW)

    print(
        f"FHEW scheme is using lattice parameter {ccLWE.Getn()},\n logQ {logQ_ccLWE},\n and modulus q {ccLWE.Getq()}\n"
    )

    # Set the scaling factor to be able to decrypt; the LWE mod switch is performed on the ciphertext at the last level
    modulus_CKKS_from = cc.GetModulusCKKS()
    pLWE1 = ccLWE.GetMaxPlaintextSpace()
    modulus_LWE = 1 << logQ_ccLWE
    beta = ccLWE.GetBeta()
    pLWE = int(modulus_LWE / (2 * beta))  # Large precision

    scFactor = cc.GetScalingFactorReal(0)
    if cc.GetScalingTechnique() == FLEXIBLEAUTOEXT:
        scFactor = cc.GetScalingFactorReal(1)
    scaleCF = int(modulus_CKKS_from) / (scFactor * pLWE)

    cc.EvalCKKStoFHEWPrecompute(scaleCF)

    # Step 3: Encoding and encryption of inputs
    x1 = [input1]
    x2 = [input2]
    encodedLength1 = len(x1)
    encodedLength2 = len(x2)

    # Encoding as plaintexts
    ptxt1 = cc.MakeCKKSPackedPlaintext(x1, 1, 0)  # , None)
    ptxt2 = cc.MakeCKKSPackedPlaintext(x2, 1, 0)  # , None)

    # Encrypt the encoded vectors
    c1 = cc.Encrypt(keys.publicKey, ptxt1)
    c2 = cc.Encrypt(keys.publicKey, ptxt2)

    # Step 4: Scheme switching from CKKS to FHEW
    cTemp1 = cc.EvalCKKStoFHEW(c1, encodedLength1)
    cTemp2 = cc.EvalCKKStoFHEW(c2, encodedLength2)
    # Step 5: Evaluate the floor function
    bits = 2
    print(
        f"\n---Decrypting switched ciphertext 1 with large precision (plaintext modulus {pLWE}) ---\n"
    )
    ptxt1.SetLength(encodedLength1)
    print(f"Input x2: {ptxt1.GetRealPackedValue()}")
    decryptLWECipherText(ccLWE, privateKeyFHEW, pLWE1, pLWE, cTemp1)

    print(
        f"\n---Decrypting switched ciphertext 2 with large precision (plaintext modulus {pLWE}) ---\n"
    )
    ptxt2.SetLength(encodedLength2)
    print(f"Input x2: {ptxt2.GetRealPackedValue()}")
    decryptLWECipherText(ccLWE, privateKeyFHEW, pLWE1, pLWE, cTemp2)

    comparator_result = comparator(cc, ccLWE, c1, c2)
    LWESign = comparator_result.get("LWESign")
    LWECiphertexts = comparator_result.get("LWECiphertexts")

    print(
        f"\n---Decrypting switched comparator_result with large precision (plaintext modulus {pLWE}) ---\n"
    )
    #  ptxt2.SetLength(encodedLength2)
    #  print(f"Output comparator_result: {ptxt2.GetRealPackedValue()}")
    decryptLWECipherText(ccLWE, privateKeyFHEW, pLWE1, pLWE, LWECiphertexts)

    fheSignNot = [ccLWE.EvalNOT(LWESign[i]) for i in range(len(LWESign))]
    print("EvalSign: ", "")
    for i in range(len(LWESign)):
        LWESignplainLWE = ccLWE.Decrypt(privateKeyFHEW, LWESign[i], 2)
        print(LWESignplainLWE, end=" ")
    print()
    print("EvalNOT: ", "")
    for i in range(len(fheSignNot)):
        plainLWE = ccLWE.Decrypt(privateKeyFHEW, fheSignNot[i], 2)
        print(plainLWE, end=" ")
    print()
    e_a1_result = [
        ccLWE.EvalBinGate(AND, cTemp1[i], LWESign[i]) for i in range(len(cTemp1))
    ]
    e_b1_result = [
        ccLWE.EvalBinGate(AND, fheSignNot[i], cTemp2[i]) for i in range(len(cTemp2))
    ]
    larger_num_circuit_result = [
        ccLWE.EvalBinGate(XOR, e_a1_result[i], e_b1_result[i])
        for i in range(len(e_b1_result))
    ]

    e_a2_result = [
        ccLWE.EvalBinGate(AND, cTemp1[i], fheSignNot[i]) for i in range(len(cTemp1))
    ]
    e_b2_result = [
        ccLWE.EvalBinGate(AND, LWESign[i], cTemp2[i]) for i in range(len(cTemp2))
    ]
    smaller_num_circuit_result = [
        ccLWE.EvalBinGate(XOR, e_a2_result[i], e_b2_result[i])
        for i in range(len(e_b2_result))
    ]
    print(
        f"\nFHEW decryption larger_num_circuit_result p = {pLWE}/(1 << bits) = {pLWE // (1 << bits)}: ",
        end="",
    )
    decryptLWECipherText(ccLWE, privateKeyFHEW, pLWE1, pLWE, larger_num_circuit_result)

    print(
        f"\nFHEW decryption smaller_num_circuit_result p = {pLWE}/(1 << bits) = {pLWE // (1 << bits)}: ",
        end="",
    )
    decryptLWECipherText(ccLWE, privateKeyFHEW, pLWE1, pLWE, smaller_num_circuit_result)

    # Step 6: Scheme switching from FHEW to CKKS
    cTemp2_cc = cc.EvalFHEWtoCKKS(
        larger_num_circuit_result,
        slots,
        slots,
    )

    plaintextDec2 = cc.Decrypt(keys.secretKey, cTemp2_cc)
    plaintextDec2.SetLength(slots)
    print(
        f"larger_num_circuit_result decryption modulus_LWE mod {pLWE // (1 << bits)}: {plaintextDec2}"
    )

    cTemp1_cc = cc.EvalFHEWtoCKKS(
        smaller_num_circuit_result,
        slots,
        slots,
    )

    plaintextDec2 = cc.Decrypt(keys.secretKey, cTemp1_cc)
    plaintextDec2.SetLength(slots)
    print(
        f"smaller_num_circuit_result decryption modulus_LWE mod {pLWE // (1 << bits)}: {plaintextDec2}"
    )


def decryptLWECipherText(ccLWE, privateKeyFHEW, pLWE1, pLWE, LWECiphertexts):
    print("FHEW Decryption")
    for j in range(len(LWECiphertexts)):
        # Decompose the large ciphertext into small ciphertexts that fit in q
        decomp = ccLWE.EvalDecomp(LWECiphertexts[j])

    # Decryption
    p = ccLWE.GetMaxPlaintextSpace()
    for i in range(len(decomp)):
        ct = decomp[i]
        if i == len(decomp) - 1:
            p = int(pLWE / (pLWE1 ** floor(log(pLWE) / log(pLWE1))))
            # The last digit should be up to P / p^floor(log_p(P))
        resultDecomp = ccLWE.Decrypt(privateKeyFHEW, ct, p)
        print(f"( {resultDecomp} * {pLWE1} ^ {i} )")
        if i != len(decomp) - 1:
            print("+", end=" ")
    print("\n")


def comparator(cc, ccLWE, ct1, ct2):
    # Compute the difference between the two ciphertexts
    cDiff = cc.EvalSub(ct1, ct2)

    # Convert the difference to the FHEW scheme
    LWECiphertexts = cc.EvalCKKStoFHEW(cDiff, 1)

    # Compute the sign of each element in LWECiphertexts
    LWESign = [None] * len(LWECiphertexts)
    for i in range(len(LWECiphertexts)):
        LWESign[i] = ccLWE.EvalSign(LWECiphertexts[i], True)

    # Return the signs and the ciphertexts
    return {"LWESign": LWESign, "LWECiphertexts": LWECiphertexts}


Can you isolate the exception from “However, I’m encountering an issue with performing gate operations between the output of EvalSign and EvalCKKStoFHEW because their moduli are different”? Where exactly is it thrown? Can you provide a smaller example (a simpler circuit where you just convert a CKKS ciphertext into a FHEW ciphertext then apply a binary gate) which reproduces the error that can be copied and run as is?

On a different note, do you have a particular need to use Boolean circuits for this? If I understand correctly what you need to output, i.e., a ciphertext consisting of the largest elements between the two inputs, and a ciphertext consisting of the smallest elements between the two inputs, then you can do it only in CKKS after computing EvalCompareSchemeSwitching. Let sign = EvalCompareSchemeSwitching(input1,input2). Then larger = (1-sign) * input1 + sign * input2, and smaller = (1-sign) * input2 + sign * input 1.

1 Like

I am getting Error

ModAddEq called on NativeVectorT’s with different parameters when ccLWE.EvalBinGate(AND, cTemp1[i], LWESign[i]) for i in range(len(cTemp1)) gets executed

I have tried to make it simple code as possible :

from lib.openfhe import *
from math import log2, floor, log


def compareStmt():
    # Step 1: Setup CryptoContext for CKKS
    scTech = FIXEDAUTO
    multDepth = 17
    if scTech == FLEXIBLEAUTOEXT:
        multDepth += 1

    firstModSize = 60
    scaleModSize = 50
    ringDim = 8192
    sl = HEStd_NotSet
    slBin = TOY
    logQ_ccLWE = 23
    slots = 16  # sparsely-packed
    batchSize = slots

    parameters = CCParamsCKKSRNS()
    parameters.SetMultiplicativeDepth(multDepth)
    parameters.SetFirstModSize(firstModSize)
    parameters.SetScalingModSize(scaleModSize)
    parameters.SetScalingTechnique(scTech)
    parameters.SetSecurityLevel(sl)
    parameters.SetRingDim(ringDim)
    parameters.SetBatchSize(batchSize)

    cc = GenCryptoContext(parameters)

    # Enable the features that you wish to use
    cc.Enable(PKE)
    cc.Enable(KEYSWITCH)
    cc.Enable(LEVELEDSHE)
    cc.Enable(ADVANCEDSHE)
    cc.Enable(SCHEMESWITCH)

    print(
        f"CKKS scheme is using ring dimension {cc.GetRingDimension()},\n number of slots {slots}, and suports a multiplicative depth of {multDepth}\n"
    )

    # Generate encryption keys.
    keys = cc.KeyGen()

    # Step 2: Prepare the FHEW cryptocontext and keys for FHEW and scheme switching
    arbFunc = False
    FHEWparams = cc.EvalSchemeSwitchingSetup(
        sl, slBin, arbFunc, logQ_ccLWE, False, slots
    )

    ccLWE = FHEWparams[0]
    privateKeyFHEW = FHEWparams[1]

    cc.EvalSchemeSwitchingKeyGen(keys, privateKeyFHEW, 1)

    # Generate bootstrapping key for EvalFloor
    ccLWE.BTKeyGen(privateKeyFHEW)

    print(
        f"FHEW scheme is using lattice parameter {ccLWE.Getn()},\n logQ {logQ_ccLWE},\n and modulus q {ccLWE.Getq()}\n"
    )

    # Set the scaling factor to be able to decrypt; the LWE mod switch is performed on the ciphertext at the last level
    modulus_CKKS_from = cc.GetModulusCKKS()
    modulus_LWE = 1 << logQ_ccLWE
    beta = ccLWE.GetBeta()
    pLWE = int(modulus_LWE / (2 * beta))  # Large precision

    scFactor = cc.GetScalingFactorReal(0)
    if cc.GetScalingTechnique() == FLEXIBLEAUTOEXT:
        scFactor = cc.GetScalingFactorReal(1)
    scaleCF = int(modulus_CKKS_from) / (scFactor * pLWE)

    cc.EvalCKKStoFHEWPrecompute(scaleCF)

    # Step 3: Encoding and encryption of inputs
    x1 = [100]
    x2 = [8]
    encodedLength1 = len(x1)

    # Encoding as plaintexts
    ptxt1 = cc.MakeCKKSPackedPlaintext(x1, 1, 0)  # , None)
    ptxt2 = cc.MakeCKKSPackedPlaintext(x2, 1, 0)  # , None)

    # Encrypt the encoded vectors
    c1 = cc.Encrypt(keys.publicKey, ptxt1)
    c2 = cc.Encrypt(keys.publicKey, ptxt2)

    # Step 4: Scheme switching from CKKS to FHEW
    cTemp1 = cc.EvalCKKStoFHEW(c1, encodedLength1)

    # Compute the difference between the two ciphertexts
    cDiff = cc.EvalSub(c1, c2)

    # Convert the difference to the FHEW scheme
    LWECiphertexts = cc.EvalCKKStoFHEW(cDiff, 1)

    # Compute the sign of each element in LWECiphertexts
    LWESign = [None] * len(LWECiphertexts)
    for i in range(len(LWECiphertexts)):
        LWESign[i] = ccLWE.EvalSign(LWECiphertexts[i], False)

    e_a1_result = [
        ccLWE.EvalBinGate(AND, cTemp1[i], LWESign[i]) for i in range(len(cTemp1))
    ]
    # Error: ModAddEq called on NativeVectorT's with different parameters.
if __name__ == "__main__":
    compareStmt()

Ok, I see the issue. EvalCKKStoFHEW returns LWE ciphertexts with a large modulus, in order to support large-precision operations, such as EvalSign. However, the EvalSign returns an LWE ciphertext with a smaller modulus. If you want to perform further operations between the output of EvalCKKStoFHEW (LWECiphertexts) and the output of EvalSign (LWESign), you need to do modulus switching for the LWECiphertexts from their larger modulus to the smaller modulus of LWESign. You can take a look here for inspiration on how to do this.

Appreciate your response. In my setup, the modulus for EvalSign is 4096, while for LWECiphertexts it’s 8388608. I examined the C++ code you referenced, but since I’m working in Python, some of the methods used there aren’t accessible in current Python wrapper. It seems like achieving this in Python might be challenging. If you have any alternative approaches in mind, I’m all ears. Let me know if you have any suggestions.

As per your suggestion i added this code in core library so i can change the modulus from higher to lower

std::vector<std::shared_ptr<LWECiphertextImpl>> SWITCHCKKSRNS::EvalFHEWChangeModulus(
    std::vector<lbcrypto::LWECiphertext> LWEciphertexts, uint32_t fromModulus, uint32_t toModulus,uint32_t numCtxts) {
        NativeInteger native_fromModulus(fromModulus);
        NativeInteger native_toModulus(toModulus);
        uint32_t n = m_ccLWE.GetParams()->GetLWEParams()->Getn();  // lattice parameter for additive LWE
#pragma omp parallel for
    for (uint32_t i = 0; i < numCtxts; i++) {
        auto original_a = LWEciphertexts[i]->GetA();
        auto original_b = LWEciphertexts[i]->GetB();
        // multiply by Q_LWE/Q' and round to Q_LWE
        NativeVector a_round(n, toModulus);
        for (uint32_t j = 0; j < n; ++j) {
            a_round[j] = RoundqQAlter(original_a[j], native_toModulus, native_fromModulus);
        }
        NativeInteger b_round = RoundqQAlter(original_b, native_toModulus, native_fromModulus);
        LWEciphertexts[i]     = std::make_shared<LWECiphertextImpl>(std::move(a_round), std::move(b_round));
    }

    return LWEciphertexts;
}

i added this in python wrapper

.def("EvalFHEWChangeModulus", &CryptoContextImpl<DCRTPoly>::EvalFHEWChangeModulus,
             cc_EvalCKKStoFHEW_docs,
             py::arg("LWEciphertexts"),
             py::arg("fromModulus"),
             py::arg("toModulus"),
             py::arg("numCtxts") = 0)

I was able to execute

    cTemp1_new =  cc.EvalFHEWChangeModulus(cTemp1,cTemp1[0].GetModulus(),LWESign[0].GetModulus(),len(cTemp1))

e_a1_result = [
        ccLWE.EvalBinGate(AND, cTemp1_new[i], LWESign[i]) for i in range(len(cTemp1))
    ]

I am not getting any errors

But i am getting incorrect outputs.

can someone please help me with this

    x1=[input1]
    x2=[input2]
    encodedLengthx1 = len(x1)
    ptxt1 = cc.MakeCKKSPackedPlaintext(x1, 1, 0, None, slots)
    ptxt2 = cc.MakeCKKSPackedPlaintext(x2, 1, 0, None, slots)

    c1 = cc.Encrypt(keys.publicKey, ptxt1)
    c2 = cc.Encrypt(keys.publicKey, ptxt2)
    cResult = cc.EvalCompareSchemeSwitching(c1, c2, slots, 0, scaleSignFHEW)

    one_minus_cResult = cc.EvalSub(1, cResult)
    # larger = (1-sign) * input1 + sign * input2
    larger = cc.EvalAdd(cc.EvalMult(one_minus_cResult, c1), cc.EvalMult(cResult, c2))
    #  smaller = (1-sign) * input2 + sign * input 1.
    smaller = cc.EvalAdd(cc.EvalMult(one_minus_cResult, c2), cc.EvalMult(cResult, c1))

    largerPlaintext = cc.Decrypt(keys.secretKey, larger)
    largerPlaintext.SetLength(encodedLengthx1)
    print(f"Decrypted larger result: {largerPlaintext}\n")

    smallerPlaintext = cc.Decrypt(keys.secretKey, smaller)
    smallerPlaintext.SetLength(encodedLengthx1)
    print(f"Decrypted smaller result: {smallerPlaintext}\n")

i tried your way of calculating
when i pass

input1 = 40 input2 = 8 , largerPlaintext = 40 and smallerPlaintext = 8

input1 = 8 input2 = 40 , largerPlaintext = 8 and smallerPlaintext = 40

can you see help me with this

First question: please read more on the FHEW/TFHE scheme to understand the message scaling and more on the scheme switching functionality to understand how the message is scaled there. An LWE valid encryption will have the message scaled by q/p. The scheme switching is designed to switch from a larger value in CKKS to its corresponding value in LWE, with a large p and a large q to support this large precision. The implemented function EvalSign returns a message scaled with a small p’=2 and q’. The Boolean gates are also currently implemented for small p’ and q’. This value of p’ cannot support the large input you want to perform AND with. You can only obtain correctness if the two LWE ciphertexts you want to operate with have the same scaling and the output is supported in the given plaintext size.

That’s why you should focus on the CKKS-based solution.
Second question: Based on the code, you don’t seem to be running with the latest version. I ran it with the latest version and I get the correct result regardless of the order, so I could not reproduce your issue.

1 Like

Thank you soo much for responding
As you said i had older version, i updated to the latest version and made some changes in the code as per new version and changed the code

cResult = cc.EvalCompareSchemeSwitching(c1, c2, slots,slots,pLWE2)

now i am getting proper results

Thank you