Performing key switching on a euint32 value passed to a contract view function

I was wondering if it is possible to perform key switching (reencrypt) on a euint32 value passed as an argument to contract view function?

I was able to get a similar bit of code working as expected, by calling a view function from the contract that stores the encrypted value as follows:

Storage.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import "fhevm/lib/TFHE.sol";

contract Storage {
    euint32 internal superSecretValue;

    constructor() {}

    function setSuperSecretValue(bytes memory v_) public returns (bool) {
        euint32 value_ = TFHE.asEuint32(v_);
        superSecretValue = value_;

        return true;
    }

    function getSuperSecretValue() public view returns (euint32) {
        return superSecretValue;
    }
}

KeySwitcher.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import "fhevm/lib/TFHE.sol";
import "./Storage.sol";

contract KeySwitcher {
    constructor() {}

    function switchKeys(
        address s_,
        euint32 value_,
        bytes32 publicKey_
    )
        public
        view
        returns (bytes memory)
    {
        return TFHE.reencrypt(Storage(s_).getSuperSecretValue(), publicKey_, 0);
    }
}

However, when I alter the KeySwitcher contract as follows the reencrypt reverts without an error string:

FaultyKeySwitcher.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import "fhevm/lib/TFHE.sol";

contract FaultyKeySwitcher {
    constructor() {}

    function switchKeys(
        euint32 value_,
        bytes32 publicKey_
    )
        public
        view
        returns (bytes memory)
    {
        return TFHE.reencrypt(value_, publicKey_, 0);
    }
}

Note that switchKeys is called from off-chain using the following script:

script.ts:

async function getNetworkPublicKey(): Promise<string> {
    const provider = ethers.provider

    // Calls the fallback function of the contract at the given address
    const publicKey = await provider.call({
        to: '0x0000000000000000000000000000000000000044'
    })

    return publicKey
}

async function createInstance(): Promise<FhevmInstance> {
    const provider = ethers.provider
    const network = await provider.getNetwork()
    const chainId = network.chainId

    const instance = await fhevmjs.createInstance({
        chainId: Number(chainId),
        publicKey: await getNetworkPublicKey()
    })

    return instance
}

async function setTokenSignature(instance: FhevmInstance, contractAddress: string, signer: Signer): Promise<void> {
    const token = instance.generateToken({
        verifyingContract: contractAddress
    })

    const signature = await signer.signTypedData(
        token.token.domain,
        { Reencrypt: token.token.types.Reencrypt },
        token.token.message
    )

    instance.setTokenSignature(contractAddress, signature)

    return
}

const encryptedValue = await storage.getSuperSecretValue()

const signer = (await ethers.getSigners())[0]

const instance = await createInstance() // Custom function for creating the instance
await setTokenSignature(instance, faultyKeySwitcher.target.toString(), signer) // Custom function for setting token signature

const token = instance.getTokenSignature(faultyKeySwitcher.target.toString())
const switchedValue = await faultyKeySwitcher.switchKeys(encryptedValue, token.publicKey) // Reverts!

Hi 0xSM,
It’s not possible to pass a valid euint value to represent a new ciphertext as an argument to a contract function that is being called from an EOA. Ciphertexts enter the system as bytes values, and then get “sanitized” by TFHE.asEuint. Only then can the resulting euint values be used as contract function arguments.

I don’t know if this fits your needs, but you could change FaultyKeySwitcher.sol to the following:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import "fhevm/lib/TFHE.sol";

contract FaultyKeySwitcher {
    constructor() {}

    function switchKeys(
        bytes calldata value_,
        bytes32 publicKey_
    )
        public
        view
        returns (bytes memory)
    {
        euint32 value = TFHE.asEuint32(value_);
        return TFHE.reencrypt(value, publicKey_, 0);
    }
}
1 Like

Hey Louis, thanks for the reply.

This would fit my needs, if I was able to retrieve the encrypted value from the Storage contract in the required format. In the above example, it is returned as a euint32 and not in bytes. Is it possible to convert this value to bytes format? Possibly by trivially encrypting it or something like that? Please see the below code snippet for reference:

function getSuperSecretValue() public view returns (euint32) {
        return superSecretValue;
}

In addition, could you tell me more about what the value returned from this function is anyway? Is it the pointer to the ciphertext?

When you send method parameters, we need to be sure that you’re not sending a copy-pasted encrypted value (copied from the blockchain state). Otherwise, you would have the possibility to decrypt any ciphertext. To check this, you send bytes which contains the encrypted value and a proof that you know the value. Note: proofs are not implemented yet but the workflow is the same.

What you want to do is to get a ciphertext from the blockchain and send it back to the blockchain, without any proof: it won’t work. However, you can do as follow:
1/ Your function getSuperSecretValue reencrypts the value with a publicKey (TFHE.reencrypt)
2/ You get this reencrypted value on client side, decrypt it and reencrypt it with fhevmjs.encrypt32(decryptedVal)
3/ You send this freshly encrypted value to your second function

This is less straightforward since you need to decrypt/encrypt on client side, but this is mandatory for security purpose.

Last thing: if you’re wondering why you don’t need a proof in your first example, it’s because of how we handle ciphertext permissions. If a contract can access a ciphertext (because the euint is public for example), they can reencrypt it, compute with it or store it. Proofs are only needed for ciphertexts coming from outside.

And last thing: yes, you can see euint32 as a pointer.

Hope it helps !

1 Like

Hey Clement, thanks for the super helpful reply.

Are you able to tell me about any methods that is exist that operate on these pointers? Either by passing it to a contract like I was trying to do, or by making requests to the node directly?

Basically, in your contract, you can use euint32 like you would use uint32. As example, you can take a look at bid function of our BlindAuction contract https://github.com/zama-ai/fhevm-solidity/blob/main/contracts/BlindAuction.sol

This bid function will get token from a ERC20 contract (transferFrom function) and add the bid. You can see that once the user’s bytes has been casted as euint32, the encryptedAmount is used as parameter for transferFrom.

And if you look at transferFrom method in our ERC20, we implement 2 methods: one with bytes (so called from clientside with js library for example) and one with euint32 (called by another smart contract). It means you can use this transferFrom method with bytes or euint32 and the correct one will be used. In the bid case, the “euint32 method” will be called. https://github.com/zama-ai/fhevm-solidity/blob/main/contracts/EncryptedERC20.sol

You can read more on function overload here Solidity - Function Overloading | Tutorialspoint

My question was more regarding use of the actual pointer itself. Is it a pointer to a storage slot of the contract? Or does it point somewhere else? Once I fetch this value from off chain, can I then pass it as an argument to some contract function which can then use the value it points to?

Yes, maybe I shouldn’t mention pointer. In your contract, this is not a pointer: this is the actual value, not a pointer to the storage slot.
I thought you were talking about implementation in the EVM where ciphertext are in fact pointer, but this is a different subject.

function getSuperSecretValue() public view returns (euint32) {
        return superSecretValue;
}

Sorry but I’m still a bit confused. When I call getSuperSecretValue() from the client, the value that is returned is the encrypted value or the pointer to the encrypted value?

I will provide full context on what I am doing, hopefully it will make things more clear on your end.

I am very interested in this project and I am trying to learn more about the security measures for maintaining the privacy of encrypted variables.

For example, it is well known that private and internal variables are easily read from the client by reading from a specific storage slot. So when I read from the storage slot of superSecretValue in the Storage contract, it returns some number in hex form (ex: 0x33679bd74625761123ea3ff4f6b1312f9b5003b1e1608a6ab43ccb21ef232f8d which is equivalent to 23251016099878316427996030519296474750471761693107816293828541072323408768909).

If this is the encrypted value, then it is my understanding that I can’t just pass it to the KeySwitcher contract because encrypted values must be in byte format. Then in theory, wouldn’t I be able to trivially encrypt it, and then pass it to the contract?

If it is the pointer to the encrypted value, then it must be either a storage slot of some contract, or perhaps it points to storage of the node itself where the encrypted value is held?

I see your confusion. When you manipulate euint32, in reality you’re manipulating a uint256. This value (the one you get, the uint256) is an internal pointer in a registry used by fhEVM. And basically, this is a hash of the real ciphertext value. When you compute on this pointer, fhEVM will replace the pointer with the ciphertext value automatically, and return a new pointer of the result (the hash of the result). This is for technical reason, and it will be explained in our whitepaper (performance and security basically).

I hope it’s more clear!

1 Like

Yep :slight_smile:

The reason we don’t don’t provide a way for users to extract the encrypted values (even though they could if really wanted to, since they’re on-chain) is because users can’t do anything useful with those. They can’t decrypt the values, because they’re encrypted under the global FHE key, and they can’t send them to another smart contract because ciphertext inputs must be accompanied with a zero-knowledge proof of knowledge (ZKPoK) of the underlying plaintext [not implemented yet].
As Clément said, this is explained thoroughly in our whitepaper which will be out soon.

1 Like

Hey Louis,

Thanks for the clarification. Great work you guys are doing! Looking forward to learning more about it when the whitepaper is published.

2 Likes