From 7364c0ee3c82ac18998ab961fc7a6337bea0a699 Mon Sep 17 00:00:00 2001 From: Petar Ivanov <29689712+dartdart26@users.noreply.github.com> Date: Fri, 21 Jul 2023 23:08:27 +0200 Subject: [PATCH 1/4] Add support for TFHE.decrypt() The decrypt() function decrypts the given euint value. There are a few considerations when doing decryption: * if a transaction is reverted due to a failed TFHE.optimisticReq(), then any decryptions would leak information as they would already have been decrypted when the txn is reverted at the end (due to the nature of TFHE.optimistiReq()) * gas estimation might be more imprecise, because control flow can now depend on decrypted values (that gas estimation doesn't know of as it always uses 0) --- codegen/codegen.py | 34 +++++++++++++++++++++++++++++++--- lib/Impl.sol | 40 +++++++++++++++++++++++++++++++++++++--- lib/Precompiles.sol | 1 + lib/TFHE.sol | 43 +++++++++++++++++++++++++++++++++++++++---- 4 files changed, 108 insertions(+), 10 deletions(-) diff --git a/codegen/codegen.py b/codegen/codegen.py index e7f5c456..27dfeb98 100644 --- a/codegen/codegen.py +++ b/codegen/codegen.py @@ -54,6 +54,7 @@ uint256 public constant Max = 88; uint256 public constant Negate = 89; uint256 public constant Not = 90; + uint256 public constant Decrypt = 91; } """ ) @@ -683,6 +684,25 @@ } } } + + function decrypt(uint256 ciphertext) internal view returns(uint256 result) { + bytes32[1] memory input; + input[0] = bytes32(ciphertext); + uint256 inputLen = 32; + + bytes32[1] memory output; + uint256 outputLen = 32; + + // Call the decrypt precompile. + uint256 precompile = Precompiles.Decrypt; + assembly { + if iszero(staticcall(gas(), precompile, input, inputLen, output, outputLen)) { + revert(0, 0) + } + } + // The output is a 32-byte buffer of a 256-bit big-endian unsigned integer. + result = uint256(output[0]); + } } """ ) @@ -982,9 +1002,9 @@ f.write(to_print_scalar.format(i=i, f="max", g="max")) -to_print_8 = """ - // If `control`'s value is 1, the result has the same value as `a`. - // If `control`'s value is 0, the result has the same value as `b`. +to_print = """ + // If `control`'s value is `true`, the result has the same value as `a`. + // If `control`'s value is `false`, the result has the same value as `b`. function cmux(ebool control, euint{i} a, euint{i} b) internal view returns (euint{i}) {{ return euint{i}.wrap(Impl.cmux(ebool.unwrap(control), euint{i}.unwrap(a), euint{i}.unwrap(b))); }} @@ -1083,6 +1103,14 @@ Impl.req(euint{i}.unwrap(value)); }} + // Decrypts the encrypted `value`. + // Note: If used with optimisticReq(), all decrypt() calls in a txn will be executed, + // but the txn might get reverted at the end. That leaks unnecessary information. + // Please use with care. + function decrypt(euint{i} value) internal view returns (uint{i}) {{ + return uint{i}(Impl.decrypt(euint{i}.unwrap(value))); + }} + // Return the negation of `value`. function neg(euint{i} value) internal view returns (euint{i}) {{ return euint{i}.wrap(Impl.neg(euint{i}.unwrap(value))); diff --git a/lib/Impl.sol b/lib/Impl.sol index a51d06c7..ea42a29e 100644 --- a/lib/Impl.sol +++ b/lib/Impl.sol @@ -426,9 +426,13 @@ library Impl { result = uint256(output[0]); } - // If `control`'s value is `true`, the result has the same value as `ifTrue`. - // If `control`'s value is `false`, the result has the same value as `ifFalse`. - function cmux(uint256 control, uint256 ifTrue, uint256 ifFalse) internal view returns (uint256 result) { + // If `control`'s value is 1, the result has the same value as `ifTrue`. + // If `control`'s value is 0, the result has the same value as `ifFalse`. + function cmux( + uint256 control, + uint256 ifTrue, + uint256 ifFalse + ) internal view returns (uint256 result) { // result = (ifTrue - ifFalse) * control + ifFalse bytes memory input = bytes.concat(bytes32(ifTrue), bytes32(ifFalse), bytes1(0x00)); uint256 inputLen = input.length; @@ -584,4 +588,34 @@ library Impl { } } } + + function decrypt( + uint256 ciphertext + ) internal view returns (uint256 result) { + bytes32[1] memory input; + input[0] = bytes32(ciphertext); + uint256 inputLen = 32; + + bytes32[1] memory output; + uint256 outputLen = 32; + + // Call the decrypt precompile. + uint256 precompile = Precompiles.Decrypt; + assembly { + if iszero( + staticcall( + gas(), + precompile, + input, + inputLen, + output, + outputLen + ) + ) { + revert(0, 0) + } + } + // The output is a 32-byte buffer of a 256-bit big-endian unsigned integer. + result = uint256(output[0]); + } } diff --git a/lib/Precompiles.sol b/lib/Precompiles.sol index 31ddf595..0226915e 100644 --- a/lib/Precompiles.sol +++ b/lib/Precompiles.sol @@ -28,4 +28,5 @@ library Precompiles { uint256 public constant Max = 88; uint256 public constant Negate = 89; uint256 public constant Not = 90; + uint256 public constant Decrypt = 91; } diff --git a/lib/TFHE.sol b/lib/TFHE.sol index 00b425bc..93fb50f3 100644 --- a/lib/TFHE.sol +++ b/lib/TFHE.sol @@ -2233,10 +2233,21 @@ library TFHE { return euint32.wrap(Impl.max(euint32.unwrap(b), uint256(a), true)); } - // If `control`'s value is 1, the result has the same value as `a`. - // If `control`'s value is 0, the result has the same value as `b`. - function cmux(ebool control, euint8 a, euint8 b) internal view returns (euint8) { - return euint8.wrap(Impl.cmux(ebool.unwrap(control), euint8.unwrap(a), euint8.unwrap(b))); + // If `control`'s value is `true`, the result has the same value as `a`. + // If `control`'s value is `false`, the result has the same value as `b`. + function cmux( + ebool control, + euint8 a, + euint8 b + ) internal view returns (euint8) { + return + euint8.wrap( + Impl.cmux( + ebool.unwrap(control), + euint8.unwrap(a), + euint8.unwrap(b) + ) + ); } // If `control`'s value is `true`, the result has the same value as `a`. @@ -2349,6 +2360,14 @@ library TFHE { Impl.req(euint8.unwrap(value)); } + // Decrypts the encrypted `value`. + // Note: If used with optimisticReq(), all decrypt() calls in a txn will be executed, + // but the txn might get reverted at the end. That leaks unnecessary information. + // Please use with care. + function decrypt(euint8 value) internal view returns (uint8) { + return uint8(Impl.decrypt(euint8.unwrap(value))); + } + // Return the negation of `value`. function neg(euint8 value) internal view returns (euint8) { return euint8.wrap(Impl.neg(euint8.unwrap(value))); @@ -2412,6 +2431,14 @@ library TFHE { Impl.req(euint16.unwrap(value)); } + // Decrypts the encrypted `value`. + // Note: If used with optimisticReq(), all decrypt() calls in a txn will be executed, + // but the txn might get reverted at the end. That leaks unnecessary information. + // Please use with care. + function decrypt(euint16 value) internal view returns (uint16) { + return uint16(Impl.decrypt(euint16.unwrap(value))); + } + // Return the negation of `value`. function neg(euint16 value) internal view returns (euint16) { return euint16.wrap(Impl.neg(euint16.unwrap(value))); @@ -2475,6 +2502,14 @@ library TFHE { Impl.req(euint32.unwrap(value)); } + // Decrypts the encrypted `value`. + // Note: If used with optimisticReq(), all decrypt() calls in a txn will be executed, + // but the txn might get reverted at the end. That leaks unnecessary information. + // Please use with care. + function decrypt(euint32 value) internal view returns (uint32) { + return uint32(Impl.decrypt(euint32.unwrap(value))); + } + // Return the negation of `value`. function neg(euint32 value) internal view returns (euint32) { return euint32.wrap(Impl.neg(euint32.unwrap(value))); From 9344133fad6924ed772658e394a3a9d60aaa7e9b Mon Sep 17 00:00:00 2001 From: Petar Ivanov <29689712+dartdart26@users.noreply.github.com> Date: Sat, 22 Jul 2023 11:40:38 +0200 Subject: [PATCH 2/4] Add support for ebool --- codegen/codegen.py | 12 +++++++----- lib/TFHE.sol | 18 ++++++------------ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/codegen/codegen.py b/codegen/codegen.py index 27dfeb98..fb75bdf9 100644 --- a/codegen/codegen.py +++ b/codegen/codegen.py @@ -1104,9 +1104,6 @@ }} // Decrypts the encrypted `value`. - // Note: If used with optimisticReq(), all decrypt() calls in a txn will be executed, - // but the txn might get reverted at the end. That leaks unnecessary information. - // Please use with care. function decrypt(euint{i} value) internal view returns (uint{i}) {{ return uint{i}(Impl.decrypt(euint{i}.unwrap(value))); }} @@ -1178,11 +1175,16 @@ function optReq(ebool b) internal view { Impl.optReq(ebool.unwrap(b)); } + + // Decrypts the encrypted `value`. + function decrypt(ebool value) internal view returns (bool) { + return bool(Impl.decrypt(ebool.unwrap(value))); + } // Converts an `ebool` to an `euint8`. - function asEuint8(ebool b) internal pure returns (euint8) {{ + function asEuint8(ebool b) internal pure returns (euint8) { return euint8.wrap(ebool.unwrap(b)); - }} + } // Reencrypt the given `value` under the given `publicKey`. // Return a serialized euint8 value. diff --git a/lib/TFHE.sol b/lib/TFHE.sol index 93fb50f3..0178849a 100644 --- a/lib/TFHE.sol +++ b/lib/TFHE.sol @@ -2361,9 +2361,6 @@ library TFHE { } // Decrypts the encrypted `value`. - // Note: If used with optimisticReq(), all decrypt() calls in a txn will be executed, - // but the txn might get reverted at the end. That leaks unnecessary information. - // Please use with care. function decrypt(euint8 value) internal view returns (uint8) { return uint8(Impl.decrypt(euint8.unwrap(value))); } @@ -2432,9 +2429,6 @@ library TFHE { } // Decrypts the encrypted `value`. - // Note: If used with optimisticReq(), all decrypt() calls in a txn will be executed, - // but the txn might get reverted at the end. That leaks unnecessary information. - // Please use with care. function decrypt(euint16 value) internal view returns (uint16) { return uint16(Impl.decrypt(euint16.unwrap(value))); } @@ -2503,9 +2497,6 @@ library TFHE { } // Decrypts the encrypted `value`. - // Note: If used with optimisticReq(), all decrypt() calls in a txn will be executed, - // but the txn might get reverted at the end. That leaks unnecessary information. - // Please use with care. function decrypt(euint32 value) internal view returns (uint32) { return uint32(Impl.decrypt(euint32.unwrap(value))); } @@ -2547,11 +2538,14 @@ library TFHE { Impl.optReq(ebool.unwrap(b)); } + // Decrypts the encrypted `value`. + function decrypt(ebool value) internal view returns (bool) { + return bool(Impl.decrypt(ebool.unwrap(value))); + } + // Converts an `ebool` to an `euint8`. function asEuint8(ebool b) internal pure returns (euint8) { - { - return euint8.wrap(ebool.unwrap(b)); - } + return euint8.wrap(ebool.unwrap(b)); } // Reencrypt the given `value` under the given `publicKey`. From df5c50a8b21ca44544ec01819103f993b03a2c92 Mon Sep 17 00:00:00 2001 From: Petar Ivanov <29689712+dartdart26@users.noreply.github.com> Date: Sat, 22 Jul 2023 11:44:29 +0200 Subject: [PATCH 3/4] Fix compilation --- codegen/codegen.py | 2 +- lib/TFHE.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codegen/codegen.py b/codegen/codegen.py index fb75bdf9..2e0cf4c1 100644 --- a/codegen/codegen.py +++ b/codegen/codegen.py @@ -1178,7 +1178,7 @@ // Decrypts the encrypted `value`. function decrypt(ebool value) internal view returns (bool) { - return bool(Impl.decrypt(ebool.unwrap(value))); + return (Impl.decrypt(ebool.unwrap(value)) != 0); } // Converts an `ebool` to an `euint8`. diff --git a/lib/TFHE.sol b/lib/TFHE.sol index 0178849a..0bd7cb50 100644 --- a/lib/TFHE.sol +++ b/lib/TFHE.sol @@ -2540,7 +2540,7 @@ library TFHE { // Decrypts the encrypted `value`. function decrypt(ebool value) internal view returns (bool) { - return bool(Impl.decrypt(ebool.unwrap(value))); + return (Impl.decrypt(ebool.unwrap(value)) != 0); } // Converts an `ebool` to an `euint8`. From e97541ecac703a90b4f59019189b403a7bcc7687 Mon Sep 17 00:00:00 2001 From: Petar Ivanov <29689712+dartdart26@users.noreply.github.com> Date: Sat, 22 Jul 2023 13:00:08 +0200 Subject: [PATCH 4/4] Remove req/optReq uint overloads --- codegen/codegen.py | 69 ++++++++++------------------------ lib/Impl.sol | 25 +++---------- lib/TFHE.sol | 93 +++++++++------------------------------------- 3 files changed, 42 insertions(+), 145 deletions(-) diff --git a/codegen/codegen.py b/codegen/codegen.py index 2e0cf4c1..08183aa6 100644 --- a/codegen/codegen.py +++ b/codegen/codegen.py @@ -1002,7 +1002,7 @@ f.write(to_print_scalar.format(i=i, f="max", g="max")) -to_print = """ +to_print_8 = """ // If `control`'s value is `true`, the result has the same value as `a`. // If `control`'s value is `false`, the result has the same value as `b`. function cmux(ebool control, euint{i} a, euint{i} b) internal view returns (euint{i}) {{ @@ -1097,12 +1097,6 @@ }} }} - // Require that the encrypted `value` is not equal to 0. - // Involves decrypting `value`. - function req(euint{i} value) internal view {{ - Impl.req(euint{i}.unwrap(value)); - }} - // Decrypts the encrypted `value`. function decrypt(euint{i} value) internal view returns (uint{i}) {{ return uint{i}(Impl.decrypt(euint{i}.unwrap(value))); @@ -1119,59 +1113,36 @@ }} """ -to_print_cast_or=""" - // Optimistically require that `value` is not equal to 0. - // - // This function does not evaluate `value` at the time of the call. - // Instead, it accumulates all optimistic requires and evaluates a single combined - // require at the end of the transaction. A side effect of this mechanism - // is that a method call with a failed optimistic require will always incur the full - // gas cost, as if all optimistic requires were true. Yet, the transaction will be - // reverted at the end if any of the optimisic requires were false. - // - // The benefit of optimistic requires is that they are faster than non-optimistic ones, - // because there is a single call to the decryption oracle per transaction, irrespective - // of how many optimistic requires were used. - function optReq(euint{i} value) internal view {{ - Impl.optReq(euint8.unwrap(asEuint8(value))); - }} -""" +for i in (2**p for p in range(3, 6)): + f.write(to_print.format(i=i)) -to_print_no_cast_or=""" - // Optimistically require that `value` is not equal to 0. +f.write("\n") +f.write("""\ + // Require that the encrypted bool `b` is true. + // Involves decrypting `b`. + function req(ebool b) internal view { + Impl.req(ebool.unwrap(b)); + } + + // Optimistically require that `b` is true. // - // This function does not evaluate `value` at the time of the call. + // This function does not evaluate `b` at the time of the call. // Instead, it accumulates all optimistic requires and evaluates a single combined // require at the end of the transaction. A side effect of this mechanism // is that a method call with a failed optimistic require will always incur the full // gas cost, as if all optimistic requires were true. Yet, the transaction will be // reverted at the end if any of the optimisic requires were false. // + // Exceptions to above rule are encrypted requires via TFHE.req() and decryption via + // TFHE.decrypt(). If either of them are encountered and if optimistic requires have been + // used before in the txn, the optimisic requires will be immediately evaluated. Rationale is + // that we want to avoid decrypting a non-optimistic require or a value if the txn is about + // to fail and be reverted anyway at the end. Checking immediately and reverting on the spot + // would avoid unnecessary decryptions. + // // The benefit of optimistic requires is that they are faster than non-optimistic ones, // because there is a single call to the decryption oracle per transaction, irrespective // of how many optimistic requires were used. - function optReq(euint{i} value) internal view {{ - Impl.optReq(euint{i}.unwrap(value)); - }} -""" - -for i in (2**p for p in range(3, 6)): - f.write(to_print.format(i=i)) - if i != 8: - f.write(to_print_cast_or.format(i=i)) - else: - f.write(to_print_no_cast_or.format(i=i)) - - -f.write("\n") -f.write("""\ - // Require that the encrypted bool `b` is not equal to 0. - // Involves decrypting `b`. - function req(ebool b) internal view { - Impl.req(ebool.unwrap(b)); - } - - // Optimistically require that `b` is not equal to 0. function optReq(ebool b) internal view { Impl.optReq(ebool.unwrap(b)); } diff --git a/lib/Impl.sol b/lib/Impl.sol index ea42a29e..5ca28074 100644 --- a/lib/Impl.sol +++ b/lib/Impl.sol @@ -426,13 +426,9 @@ library Impl { result = uint256(output[0]); } - // If `control`'s value is 1, the result has the same value as `ifTrue`. - // If `control`'s value is 0, the result has the same value as `ifFalse`. - function cmux( - uint256 control, - uint256 ifTrue, - uint256 ifFalse - ) internal view returns (uint256 result) { + // If `control`'s value is `true`, the result has the same value as `ifTrue`. + // If `control`'s value is `false`, the result has the same value as `ifFalse`. + function cmux(uint256 control, uint256 ifTrue, uint256 ifFalse) internal view returns (uint256 result) { // result = (ifTrue - ifFalse) * control + ifFalse bytes memory input = bytes.concat(bytes32(ifTrue), bytes32(ifFalse), bytes1(0x00)); uint256 inputLen = input.length; @@ -589,9 +585,7 @@ library Impl { } } - function decrypt( - uint256 ciphertext - ) internal view returns (uint256 result) { + function decrypt(uint256 ciphertext) internal view returns (uint256 result) { bytes32[1] memory input; input[0] = bytes32(ciphertext); uint256 inputLen = 32; @@ -602,16 +596,7 @@ library Impl { // Call the decrypt precompile. uint256 precompile = Precompiles.Decrypt; assembly { - if iszero( - staticcall( - gas(), - precompile, - input, - inputLen, - output, - outputLen - ) - ) { + if iszero(staticcall(gas(), precompile, input, inputLen, output, outputLen)) { revert(0, 0) } } diff --git a/lib/TFHE.sol b/lib/TFHE.sol index 0bd7cb50..389af4a5 100644 --- a/lib/TFHE.sol +++ b/lib/TFHE.sol @@ -2235,19 +2235,8 @@ library TFHE { // If `control`'s value is `true`, the result has the same value as `a`. // If `control`'s value is `false`, the result has the same value as `b`. - function cmux( - ebool control, - euint8 a, - euint8 b - ) internal view returns (euint8) { - return - euint8.wrap( - Impl.cmux( - ebool.unwrap(control), - euint8.unwrap(a), - euint8.unwrap(b) - ) - ); + function cmux(ebool control, euint8 a, euint8 b) internal view returns (euint8) { + return euint8.wrap(Impl.cmux(ebool.unwrap(control), euint8.unwrap(a), euint8.unwrap(b))); } // If `control`'s value is `true`, the result has the same value as `a`. @@ -2354,12 +2343,6 @@ library TFHE { } } - // Require that the encrypted `value` is not equal to 0. - // Involves decrypting `value`. - function req(euint8 value) internal view { - Impl.req(euint8.unwrap(value)); - } - // Decrypts the encrypted `value`. function decrypt(euint8 value) internal view returns (uint8) { return uint8(Impl.decrypt(euint8.unwrap(value))); @@ -2375,22 +2358,6 @@ library TFHE { return euint8.wrap(Impl.not(euint8.unwrap(value))); } - // Optimistically require that `value` is not equal to 0. - // - // This function does not evaluate `value` at the time of the call. - // Instead, it accumulates all optimistic requires and evaluates a single combined - // require at the end of the transaction. A side effect of this mechanism - // is that a method call with a failed optimistic require will always incur the full - // gas cost, as if all optimistic requires were true. Yet, the transaction will be - // reverted at the end if any of the optimisic requires were false. - // - // The benefit of optimistic requires is that they are faster than non-optimistic ones, - // because there is a single call to the decryption oracle per transaction, irrespective - // of how many optimistic requires were used. - function optReq(euint8 value) internal view { - Impl.optReq(euint8.unwrap(value)); - } - // Convert a serialized `ciphertext` to an encrypted euint16 integer. function asEuint16(bytes memory ciphertext) internal view returns (euint16) { return euint16.wrap(Impl.verify(ciphertext, Common.euint16_t)); @@ -2422,12 +2389,6 @@ library TFHE { } } - // Require that the encrypted `value` is not equal to 0. - // Involves decrypting `value`. - function req(euint16 value) internal view { - Impl.req(euint16.unwrap(value)); - } - // Decrypts the encrypted `value`. function decrypt(euint16 value) internal view returns (uint16) { return uint16(Impl.decrypt(euint16.unwrap(value))); @@ -2443,22 +2404,6 @@ library TFHE { return euint16.wrap(Impl.not(euint16.unwrap(value))); } - // Optimistically require that `value` is not equal to 0. - // - // This function does not evaluate `value` at the time of the call. - // Instead, it accumulates all optimistic requires and evaluates a single combined - // require at the end of the transaction. A side effect of this mechanism - // is that a method call with a failed optimistic require will always incur the full - // gas cost, as if all optimistic requires were true. Yet, the transaction will be - // reverted at the end if any of the optimisic requires were false. - // - // The benefit of optimistic requires is that they are faster than non-optimistic ones, - // because there is a single call to the decryption oracle per transaction, irrespective - // of how many optimistic requires were used. - function optReq(euint16 value) internal view { - Impl.optReq(euint8.unwrap(asEuint8(value))); - } - // Convert a serialized `ciphertext` to an encrypted euint32 integer. function asEuint32(bytes memory ciphertext) internal view returns (euint32) { return euint32.wrap(Impl.verify(ciphertext, Common.euint32_t)); @@ -2490,12 +2435,6 @@ library TFHE { } } - // Require that the encrypted `value` is not equal to 0. - // Involves decrypting `value`. - function req(euint32 value) internal view { - Impl.req(euint32.unwrap(value)); - } - // Decrypts the encrypted `value`. function decrypt(euint32 value) internal view returns (uint32) { return uint32(Impl.decrypt(euint32.unwrap(value))); @@ -2511,29 +2450,31 @@ library TFHE { return euint32.wrap(Impl.not(euint32.unwrap(value))); } - // Optimistically require that `value` is not equal to 0. + // Require that the encrypted bool `b` is true. + // Involves decrypting `b`. + function req(ebool b) internal view { + Impl.req(ebool.unwrap(b)); + } + + // Optimistically require that `b` is true. // - // This function does not evaluate `value` at the time of the call. + // This function does not evaluate `b` at the time of the call. // Instead, it accumulates all optimistic requires and evaluates a single combined // require at the end of the transaction. A side effect of this mechanism // is that a method call with a failed optimistic require will always incur the full // gas cost, as if all optimistic requires were true. Yet, the transaction will be // reverted at the end if any of the optimisic requires were false. // + // Exceptions to above rule are encrypted requires via TFHE.req() and decryption via + // TFHE.decrypt(). If either of them are encountered and if optimistic requires have been + // used before in the txn, the optimisic requires will be immediately evaluated. Rationale is + // that we want to avoid decrypting a non-optimistic require or a value if the txn is about + // to fail and be reverted anyway at the end. Checking immediately and reverting on the spot + // would avoid unnecessary decryptions. + // // The benefit of optimistic requires is that they are faster than non-optimistic ones, // because there is a single call to the decryption oracle per transaction, irrespective // of how many optimistic requires were used. - function optReq(euint32 value) internal view { - Impl.optReq(euint8.unwrap(asEuint8(value))); - } - - // Require that the encrypted bool `b` is not equal to 0. - // Involves decrypting `b`. - function req(ebool b) internal view { - Impl.req(ebool.unwrap(b)); - } - - // Optimistically require that `b` is not equal to 0. function optReq(ebool b) internal view { Impl.optReq(ebool.unwrap(b)); }