ibarrond / Pyfhel

PYthon For Homomorphic Encryption Libraries, perform encrypted computations such as sum, mult, scalar product or matrix multiplication in Python, with NumPy compatibility. Uses SEAL/PALISADE as backends, implemented using Cython.
https://pyfhel.readthedocs.io/
Apache License 2.0
482 stars 78 forks source link

Value Error: encrypted1 and encrypted2 parameter mismatch #240

Open eduardo-sarmento opened 4 months ago

eduardo-sarmento commented 4 months ago

Description Hello. I have been trying to implement the Central Kernal Aligment(CKA) similarity with Fully Homomorphic Encryption using Pyfhel. However, I have been running into the Value Error: encrypted1 and encrypted2 parameter mismatch in the last line of the implementation (result = top*bottom). I read in the documentation that using the rescale_to_next and align_mod_n_scale methods can resolve this problem, but when I use them I get a "cannot align scales 30 and 120: available rescaling [30 60]" warning. Most strangely is that sometimes it works when I use those methods, and it keeps working for a time, but if I try the same code latter it suddenly stops working.

Code To Reproduce Error

def cka_unecrypted(X, Y, XTX, YTY):
    # Implements linear CKA as in Kornblith et al. (2019)
    X = X.copy()
    Y = Y.copy()
    YTX = Y.T.dot(X)
    return (YTX**2).sum() * XTX * YTY

def cka_encrypted(X, Y, XTX, YTY, HE):
    X = X.copy()
    Y = Y.copy()
    if len(X) == len(Y) == 1:
        YTX = X[0] @ Y[0]
    else:
        YTX = [~(X[i] * Y[i]) for i in range(len(X[i]))]
        for i in range(1, len(YTX)):
            YTX[0] += YTX[i]
        YTX = HE.cumul_add(YTX[0])
    bottom = XTX * YTY

    HE.relinearize(bottom)

    square = YTX * YTX
    HE.relinearize(square)

    top = HE.cumul_add(square, False, 1)
    HE.relinearize(top)
    # Tried reescales
    # HE.rescale_to_next(bottom)
    # HE.rescale_to_next(top)
    # top, bottom = HE.align_mod_n_scale(bottom,top)

    # problem line
    result = bottom * top
    HE.relinearize(result)
    return result

def cka(X, Y, XTX, YTY, HE=None, crypt=False):
    if crypt:
        res = cka_encrypted(X, Y, XTX, YTY, HE)
    else:
        res = cka_unecrypted(X, Y, XTX, YTY)
    return res

rng = np.random.default_rng(42)
CASE_SELECTOR = 1  # 1 or 2

case_params = {
    1: {"l": 256},  # small l
    2: {"l": 65536},  # large l
}[CASE_SELECTOR]
l = case_params["l"]
a = rng.normal(loc=0, scale=1, size=l)
b = rng.normal(loc=0, scale=1, size=l)
a_centralized = a - a.mean(axis=0)
b_centralized = b - b.mean(axis=0)
ATA = 1 / np.sqrt((a_centralized.T.dot(a_centralized) ** 2).sum())
BTB = 1 / np.sqrt((b_centralized.T.dot(b_centralized) ** 2).sum())

bitsize = lambda x: np.ceil(np.log2(x))
get_closest_power_of_two = lambda x: int(2 ** (bitsize(x)))

def get_CKKS_context_scalar_prod(
    l: int, sec: int = 128, use_n_min: bool = True
) -> Pyfhel:
    """
    Returns the best context parameters to compute scalar product in CKKS scheme.

    The important context parameters to set are:
    - n: the polynomial modulus degree (= 2*n_slots)

    *Optimal n*: Chosen among {2**12, 2**13, 2**14, 2**15}.
        The bigger n, the more secure the scheme, but the slower the computations.
        It might be faster to use n<l and have multiple ciphertexts pervector.

    Arguments:
        l: vector length
        v_max: max element value

    Returns:
        Pyfhel: context to perform homomorphic encryption
    """
    # > OPTIMAL n
    n_min = 2**14
    if use_n_min:
        n = n_min  # use n_min regardless of l
    elif 2 * l < n_min:
        n = n_min  # Smallest
    elif 2 * l > 2**15:
        n = 2**15  # Largest
    else:
        n = get_closest_power_of_two(2 * l)

    context_params = {
        "scheme": "CKKS",
        "n": n,  # Poly modulus degree. BFV ptxt is a n//2 by 2 matrix.
        "sec": sec,  # Security level.
        "scale": 2**30,
        "qi_sizes": [60] + 10 * [30] + [60],  # Max number of multiplications = 1
    }
    HE = Pyfhel(context_params)
    return HE

HE = get_CKKS_context_scalar_prod(l, sec=128, use_n_min=True)
HE.keyGen()
HE.relinKeyGen()
HE.rotateKeyGen()

ctxt_a = [
    HE.encrypt(a_centralized[j : j + HE.get_nSlots()])
    for j in range(0, l, HE.get_nSlots())
]
ctxt_b = [
    HE.encrypt(b_centralized[j : j + HE.get_nSlots()])
    for j in range(0, l, HE.get_nSlots())
]
ctxt_a_transposed = [
    HE.encrypt(a_centralized.T[j : j + HE.get_nSlots()])
    for j in range(0, l, HE.get_nSlots())
]
ctxt_b_transposed = [
    HE.encrypt(b_centralized.T[j : j + HE.get_nSlots()])
    for j in range(0, l, HE.get_nSlots())
]
ctxt_ATA = HE.encrypt(ATA)
ctxt_BTB = HE.encrypt(BTB)

cka_ab = cka(
    ctxt_a, ctxt_b_transposed, ctxt_ATA, ctxt_BTB, HE, crypt=True
)  
print(HE.decrypt(cka_ab)[0])

ValueError Traceback (most recent call last) in <cell line: 133>() 131 ctxt_BTB = HE.encrypt(BTB) 132 --> 133 cka_ab = cka(ctxt_a, ctxt_b_transposed, ctxt_ATA, ctxt_BTB, HE , crypt=True)#cka(ctxt_a, ctxt_b_transposed, ctxt_ATA, ctxt_BTB, HE , crypt=True) 134 print(HE.decrypt(cka_ab)[0])

1 frames in cka_encrypted(X, Y, XTX, YTY, HE) 50 #print(bottom) 51 ---> 52 result = bottom * top 53 #print("result encrypted") 54 #print(HE.decrypt(result))

Pyfhel/PyCtxt.pyx in Pyfhel.PyCtxt.PyCtxt.mul()

Pyfhel/Pyfhel.pyx in Pyfhel.Pyfhel.Pyfhel.multiply()

ValueError: encrypted1 and encrypted2 parameter mismatch Setup: This setup is being run in a Docker container, but this problem has presented itself when I run it using google colab as well

ShokofehVS commented 4 months ago

Hello, I have a feeling that your problem can be resolved by a solution described here.

eduardo-sarmento commented 4 months ago

I don't think so, in the link that you shared in the error the list of available reescalings was empty, but in my case the list has reescalings, [30 60]. I also tested with more qi_sizes, up to 20*[30], but it did not have any effect on the error.