Skip to content

feat(coprocessor): add FHE.sum native opcode (op 28) end-to-end#2274

Open
PanGan21 wants to merge 11 commits intomainfrom
panos/implement-sum-operator
Open

feat(coprocessor): add FHE.sum native opcode (op 28) end-to-end#2274
PanGan21 wants to merge 11 commits intomainfrom
panos/implement-sum-operator

Conversation

@PanGan21
Copy link
Copy Markdown
Contributor

@PanGan21 PanGan21 commented Apr 9, 2026

Summary

  • Adds FHE.sum as a native coprocessor opcode (op 28). Solidity emits a single FheSum event carrying the full input array; the coprocessor executes the sum using tfhe-rs.
  • Supported types: euint8, euint16, euint32, euint64, euint128.
  • Array size limits: 100 elements for Uint8–Uint32, 60 for Uint64–Uint128.

Closes: https://github.com/zama-ai/fhevm-internal/issues/1226

@cla-bot cla-bot bot added the cla-signed label Apr 9, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 9, 2026

Coprocessor Changed Lines Coverage

Coverage of added/modified lines in coprocessor: 62.0%

Per-file breakdown

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • coprocessor/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs (62.8%): Missing lines 725-731,826-840,3202,3204-3208,3211-3215,3218-3222,3225-3229,3232-3236,3239-3240,3613
  • coprocessor/fhevm-engine/fhevm-engine-common/src/types.rs (100%)
  • coprocessor/fhevm-engine/host-listener/src/database/tfhe_event_propagate.rs (40.0%): Missing lines 1025,1061,1271

Summary

  • Total: 145 lines
  • Missing: 54 lines
  • Coverage: 62%

coprocessor/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs

  721                     let fhe_sum_max_inputs = match first_type {
  722                         5 | 6 => FHE_SUM_MAX_INPUTS_WIDE,   // Uint64 | Uint128
  723                         2..=4 => FHE_SUM_MAX_INPUTS_NARROW, // Uint8 | Uint16 | Uint32
  724                         _ => {
! 725                             return Err(FhevmError::UnsupportedFheTypes {
! 726                                 fhe_operation: format!(
! 727                                     "{:?}: type {first_type} is not supported for FheSum",
! 728                                     fhe_op
! 729                                 ),
! 730                                 input_types: vec![],
! 731                             })
  732                         }
  733                     };
  734 
  735                     if input_handles.len() > fhe_sum_max_inputs {

  822     release_memory_on_gpu(gpu_mem_res, gpu_idx);
  823     res
  824 }
  825 
! 826 fn collect_operands_as<'a, T>(
! 827     fhe_operation: &SupportedFheOperations,
! 828     operands: &'a [SupportedFheCiphertexts],
! 829     extract: impl Fn(&'a SupportedFheCiphertexts) -> Option<&'a T>,
! 830 ) -> Result<Vec<&'a T>, FhevmError> {
! 831     operands
! 832         .iter()
! 833         .map(|op| {
! 834             extract(op).ok_or_else(|| FhevmError::UnsupportedFheTypes {
! 835                 fhe_operation: format!("{:?}", fhe_operation),
! 836                 input_types: operands.iter().map(|i| i.type_name()).collect(),
! 837             })
! 838         })
! 839         .collect()
! 840 }
  841 
  842 pub fn perform_fhe_operation_impl(
  843     fhe_operation_int: i16,
  844     input_operands: &[SupportedFheCiphertexts],

  3198         SupportedFheOperations::FheGetInputCiphertext => Err(FhevmError::UnsupportedFheTypes {
  3199             fhe_operation: format!("{:?}", fhe_operation),
  3200             input_types: input_operands.iter().map(|i| i.type_name()).collect(),
  3201         }),
! 3202         SupportedFheOperations::FheSum => match &input_operands[0] {
  3203             SupportedFheCiphertexts::FheUint8(_) => {
! 3204                 collect_operands_as(&fhe_operation, input_operands, |op| match op {
! 3205                     SupportedFheCiphertexts::FheUint8(v) => Some(v),
! 3206                     _ => None,
! 3207                 })
! 3208                 .map(|refs| SupportedFheCiphertexts::FheUint8(refs.into_iter().sum()))
  3209             }
  3210             SupportedFheCiphertexts::FheUint16(_) => {
! 3211                 collect_operands_as(&fhe_operation, input_operands, |op| match op {
! 3212                     SupportedFheCiphertexts::FheUint16(v) => Some(v),
! 3213                     _ => None,
! 3214                 })
! 3215                 .map(|refs| SupportedFheCiphertexts::FheUint16(refs.into_iter().sum()))
  3216             }
  3217             SupportedFheCiphertexts::FheUint32(_) => {
! 3218                 collect_operands_as(&fhe_operation, input_operands, |op| match op {
! 3219                     SupportedFheCiphertexts::FheUint32(v) => Some(v),
! 3220                     _ => None,
! 3221                 })
! 3222                 .map(|refs| SupportedFheCiphertexts::FheUint32(refs.into_iter().sum()))
  3223             }
  3224             SupportedFheCiphertexts::FheUint64(_) => {
! 3225                 collect_operands_as(&fhe_operation, input_operands, |op| match op {
! 3226                     SupportedFheCiphertexts::FheUint64(v) => Some(v),
! 3227                     _ => None,
! 3228                 })
! 3229                 .map(|refs| SupportedFheCiphertexts::FheUint64(refs.into_iter().sum()))
  3230             }
  3231             SupportedFheCiphertexts::FheUint128(_) => {
! 3232                 collect_operands_as(&fhe_operation, input_operands, |op| match op {
! 3233                     SupportedFheCiphertexts::FheUint128(v) => Some(v),
! 3234                     _ => None,
! 3235                 })
! 3236                 .map(|refs| SupportedFheCiphertexts::FheUint128(refs.into_iter().sum()))
  3237             }
  3238             _ => Err(FhevmError::UnsupportedFheTypes {
! 3239                 fhe_operation: format!("{:?}", fhe_operation),
! 3240                 input_types: input_operands.iter().map(|i| i.type_name()).collect(),
  3241             }),
  3242         },
  3243     }
  3244 }

  3609         let handles = vec![h0, h1];
  3610         let scalars = vec![false, false];
  3611         assert!(
  3612             check_fhe_operand_types(FHE_SUM_OP, &handles, &scalars).is_err(),
! 3613             "mixed types should fail"
  3614         );
  3615     }
  3616 }

coprocessor/fhevm-engine/host-listener/src/database/tfhe_event_propagate.rs

  1021         E::TrivialEncrypt(_) => O::FheTrivialEncrypt as i32,
  1022         E::FheIfThenElse(_) => O::FheIfThenElse as i32,
  1023         E::FheRand(_) => O::FheRand as i32,
  1024         E::FheRandBounded(_) => O::FheRandBounded as i32,
! 1025         E::FheSum(_) => O::FheSum as i32,
  1026         // Not tfhe ops
  1027         E::Initialized(_) | E::Upgraded(_) | E::VerifyInput(_) => -1,
  1028     }
  1029 }

  1057         E::TrivialEncrypt(_) => "FheTrivialEncrypt",
  1058         E::FheIfThenElse(_) => "FheIfThenElse",
  1059         E::FheRand(_) => "FheRand",
  1060         E::FheRandBounded(_) => "FheRandBounded",
! 1061         E::FheSum(_) => "FheSum",
  1062         E::Initialized(_) => "Initialized",
  1063         E::Upgraded(_) => "Upgraded",
  1064         E::VerifyInput(_) => "VerifyInput",
  1065     }

  1267         }
  1268 
  1269         E::FheRand(_) | E::FheRandBounded(_) | E::TrivialEncrypt(_) => vec![],
  1270 
! 1271         E::FheSum(C::FheSum { values, .. }) => values.clone(),
  1272 
  1273         E::Initialized(_) | E::Upgraded(_) | E::VerifyInput(_) => vec![],
  1274     }
  1275 }

@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch 4 times, most recently from f7bffd5 to 08912b5 Compare April 9, 2026 15:05
@zama-ai zama-ai deleted a comment from claude bot Apr 9, 2026
@zama-ai zama-ai deleted a comment from claude bot Apr 9, 2026
@zama-ai zama-ai deleted a comment from claude bot Apr 9, 2026
@PanGan21 PanGan21 added e2e When set on a PR, will enable & trigger a run of e2e tests. e2e orchestrate test Run the test-suite-orchestrate-e2e-tests on a PR (instead of merge-queue) labels Apr 9, 2026
@zama-ai zama-ai deleted a comment from claude bot Apr 9, 2026
claude[bot]

This comment was marked as resolved.

Comment thread coprocessor/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs
@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch from 9cbb51a to 80a4f8d Compare April 11, 2026 16:39
@PanGan21 PanGan21 changed the title feat(coprocessor): add native FHE_SUM opcode (op 35) end-to-end feat(coprocessor): add FHE.sum native opcode (op 35) end-to-end Apr 11, 2026
@PanGan21 PanGan21 marked this pull request as ready for review April 13, 2026 07:12
@PanGan21 PanGan21 requested review from a team as code owners April 13, 2026 07:12
Comment thread coprocessor/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs Outdated
@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch from 80a4f8d to d44bb67 Compare April 14, 2026 06:44
@PanGan21 PanGan21 requested a review from obatirou April 14, 2026 06:44
Comment thread host-contracts/examples/tests/FHEVMManualTestSuite.sol
Comment thread host-contracts/test/fhevmOperations/manual.ts
@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch from d44bb67 to b550cc9 Compare April 14, 2026 09:28
@obatirou
Copy link
Copy Markdown
Contributor

Were make update-conformance && make prettier run?

@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch from b550cc9 to ba98167 Compare April 14, 2026 10:59
Comment thread host-contracts/contracts/FHEVMExecutor.sol
@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch from ba98167 to 52c0638 Compare April 14, 2026 13:06
Comment thread coprocessor/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs
Comment thread coprocessor/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs Outdated
Comment thread coprocessor/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs
Comment thread coprocessor/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs Outdated
Comment thread coprocessor/proto/common.proto
Comment thread host-contracts/contracts/HCULimit.sol

function sum(bytes32[] memory values) internal returns (bytes32 result) {
CoprocessorConfig storage $ = getCoprocessorConfig();
result = IFHEVMExecutor($.CoprocessorAddress).fheSum(values);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here it could degenerate case (size 1 or 2)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check is already happening here:

if (values.length < 2) revert FHECollectionSizeInvalid(values.length, 2);
uint256 supportedTypes = (1 << uint8(FheType.Uint8)) +
(1 << uint8(FheType.Uint16)) +
(1 << uint8(FheType.Uint32)) +
(1 << uint8(FheType.Uint64)) +
(1 << uint8(FheType.Uint128));
FheType resultType = _verifyAndReturnType(values[0], supportedTypes);
uint256 maxSize;
if (resultType == FheType.Uint64 || resultType == FheType.Uint128) {
maxSize = 60;
} else {
maxSize = 100;
}
if (values.length > maxSize) revert FHECollectionSizeInvalid(values.length, maxSize);

I think there is no need to add it twice in the function body. It will also duplicate maintenance if we would like to change something

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry the comment was unclear, I think we should handle degenerate case 0 (sum([]) = 0), 1 and 2. S
E.g. a contract handling a table that start empty and grows and does a sum on it, it forces a developer to handle size 0 1 and 2 manually but there is no real reason for that.

Copy link
Copy Markdown
Contributor Author

@PanGan21 PanGan21 Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @obatirou we need a second opinion here.
Would you say it is best to handle empty array or array with one element or throw error?
@rudy-6-4 has a point here that when writing generic code these are valid operations.

python
Python 3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> sum([])
0
>>> sum([1])
1
>>> sum([2])
2
>>> exit()

Additionally to that in case of 0 do you think we should return trivially encrypted 0?

Copy link
Copy Markdown
Contributor Author

@PanGan21 PanGan21 Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for flagging it @rudy-6-4 . Discussed this offline and agreed the following:

Uninitialized element in the array: Substitute with trivialEncrypt(0) — consistent with how binary ops handle uninitialized inputs, but leaks metadata
Array length of 0 or 1: Substitute — length 0 substitute with trivialEncrypt(0) and length 1 substitute with trivialEncrypt(val[0])

Let me know what you think. A small drawback to this approach is that we send computation to copro for handles that are already predicted.

rudy-6-4
rudy-6-4 previously approved these changes Apr 15, 2026
@PanGan21 PanGan21 changed the title feat(coprocessor): add FHE.sum native opcode (op 35) end-to-end feat(coprocessor): add FHE.sum native opcode (op 28) end-to-end Apr 16, 2026
@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch 2 times, most recently from 557fc51 to cf2d9db Compare April 16, 2026 08:36
@zama-ai zama-ai deleted a comment from claude bot Apr 16, 2026
@zama-ai zama-ai deleted a comment from claude bot Apr 16, 2026
@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch from cf2d9db to c69a30d Compare April 16, 2026 08:41
@PanGan21 PanGan21 requested a review from rudy-6-4 April 16, 2026 11:33
@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch from c69a30d to cd69a64 Compare April 16, 2026 12:10
@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch from e622224 to 462eaad Compare April 17, 2026 13:00
@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch from 462eaad to fc2a141 Compare April 17, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed e2e orchestrate test Run the test-suite-orchestrate-e2e-tests on a PR (instead of merge-queue) e2e When set on a PR, will enable & trigger a run of e2e tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants