Skip to content

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

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

feat(coprocessor): add FHE.sum native opcode (op 28) end-to-end#2274
PanGan21 wants to merge 14 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: 58.0%

Per-file breakdown

Diff Coverage

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

  • coprocessor/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs (57.1%): Missing lines 719-725,820-834,3197-3209,3211-3215,3218-3222,3225-3229,3232-3236,3239-3243,3246-3247,3621
  • 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
  • coprocessor/fhevm-engine/scheduler/src/dfg/scheduler.rs (66.7%): Missing lines 462-466

Summary

  • Total: 170 lines
  • Missing: 71 lines
  • Coverage: 58%

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

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

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

  3193             fhe_operation: format!("{:?}", fhe_operation),
  3194             input_types: input_operands.iter().map(|i| i.type_name()).collect(),
  3195         }),
  3196         SupportedFheOperations::FheSum => {
! 3197             if input_operands.is_empty() {
! 3198                 if !matches!(output_type, 2..=6) {
! 3199                     return Err(FhevmError::UnsupportedFheTypes {
! 3200                         fhe_operation: format!(
! 3201                             "{:?}: type {output_type} is not supported for FheSum",
! 3202                             fhe_operation
! 3203                         ),
! 3204                         input_types: vec![],
! 3205                     });
! 3206                 }
! 3207                 return trivial_encrypt_be_bytes(output_type, &[0u8]);
! 3208             }
! 3209             match &input_operands[0] {
  3210                 SupportedFheCiphertexts::FheUint8(_) => {
! 3211                     collect_operands_as(&fhe_operation, input_operands, |op| match op {
! 3212                         SupportedFheCiphertexts::FheUint8(v) => Some(v),
! 3213                         _ => None,
! 3214                     })
! 3215                     .map(|refs| SupportedFheCiphertexts::FheUint8(refs.into_iter().sum()))
  3216                 }
  3217                 SupportedFheCiphertexts::FheUint16(_) => {
! 3218                     collect_operands_as(&fhe_operation, input_operands, |op| match op {
! 3219                         SupportedFheCiphertexts::FheUint16(v) => Some(v),
! 3220                         _ => None,
! 3221                     })
! 3222                     .map(|refs| SupportedFheCiphertexts::FheUint16(refs.into_iter().sum()))
  3223                 }
  3224                 SupportedFheCiphertexts::FheUint32(_) => {
! 3225                     collect_operands_as(&fhe_operation, input_operands, |op| match op {
! 3226                         SupportedFheCiphertexts::FheUint32(v) => Some(v),
! 3227                         _ => None,
! 3228                     })
! 3229                     .map(|refs| SupportedFheCiphertexts::FheUint32(refs.into_iter().sum()))
  3230                 }
  3231                 SupportedFheCiphertexts::FheUint64(_) => {
! 3232                     collect_operands_as(&fhe_operation, input_operands, |op| match op {
! 3233                         SupportedFheCiphertexts::FheUint64(v) => Some(v),
! 3234                         _ => None,
! 3235                     })
! 3236                     .map(|refs| SupportedFheCiphertexts::FheUint64(refs.into_iter().sum()))
  3237                 }
  3238                 SupportedFheCiphertexts::FheUint128(_) => {
! 3239                     collect_operands_as(&fhe_operation, input_operands, |op| match op {
! 3240                         SupportedFheCiphertexts::FheUint128(v) => Some(v),
! 3241                         _ => None,
! 3242                     })
! 3243                     .map(|refs| SupportedFheCiphertexts::FheUint128(refs.into_iter().sum()))
  3244                 }
  3245                 _ => Err(FhevmError::UnsupportedFheTypes {
! 3246                     fhe_operation: format!("{:?}", fhe_operation),
! 3247                     input_types: input_operands.iter().map(|i| i.type_name()).collect(),
  3248                 }),
  3249             }
  3250         }
  3251     }

  3617         let handles = vec![h0, h1];
  3618         let scalars = vec![false, false];
  3619         assert!(
  3620             check_fhe_operand_types(FHE_SUM_OP, &handles, &scalars).is_err(),
! 3621             "mixed types should fail"
  3622         );
  3623     }
  3624 }

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 }

coprocessor/fhevm-engine/scheduler/src/dfg/scheduler.rs

  458         RERAND_LATENCY_BATCH_HISTOGRAM.observe(elapsed.as_secs_f64());
  459     }
  460     let opcode = node.opcode;
  461     let output_type = get_ct_type(&node.result_handle).map_err(|e| {
! 462         error!(target: "scheduler", { handle = ?hex::encode(&node.result_handle), error = ?e },
! 463                "Invalid result handle: cannot read type byte");
! 464         telemetry::set_current_span_error(&e);
! 465         SchedulerError::SchedulerError
! 466     })?;
  467 
  468     let result = std::panic::catch_unwind(|| {
  469         run_computation(
  470             opcode,

@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 Outdated
@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 Outdated
@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch from ba98167 to 52c0638 Compare April 14, 2026 13:06
@PanGan21 PanGan21 requested a review from rudy-6-4 April 20, 2026 16:12
@zama-ai zama-ai deleted a comment from claude bot Apr 20, 2026
@zama-ai zama-ai deleted a comment from claude bot Apr 20, 2026
Comment thread coprocessor/fhevm-engine/fhevm-engine-common/src/tfhe_ops.rs
@zama-ai zama-ai deleted a comment from claude bot Apr 20, 2026
}

result = keccak256(
bytes32 preimage = keccak256(
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.

One of the goal was to share the handle generation between all nary op. (like other ops)

Copy link
Copy Markdown
Contributor Author

@PanGan21 PanGan21 Apr 20, 2026

Choose a reason for hiding this comment

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

Well it will not be the same preimage, because the resultType used for sum is not needed for other other opearations (i.e. isIn).
For sum the resultType is used so corpocessor knows what will be the result, however this is not needed to be known for bolean operations.

Do you think it is better to have common preimage although it is not used always?

rudy-6-4
rudy-6-4 previously approved these changes Apr 20, 2026
@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch from 13e3ee4 to 3b32f80 Compare April 20, 2026 16:33
@PanGan21 PanGan21 force-pushed the panos/implement-sum-operator branch from 3b32f80 to 3bc002e Compare April 20, 2026 16:38
@PanGan21 PanGan21 requested a review from rudy-6-4 April 20, 2026 16:40
@zama-ai zama-ai deleted a comment from claude bot Apr 20, 2026
@PanGan21
Copy link
Copy Markdown
Contributor Author

@claude /pr-review

@claude
Copy link
Copy Markdown

claude bot commented Apr 20, 2026

Claude finished @PanGan21's request.View run

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

[AUTOMATED]

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

Summary

Thorough review covering bug hunting (2× opus passes), guideline compliance, and error handling audit across all 37 changed files. This is a well-implemented end-to-end feature addition.

Verdict: ✅ Approve

No blocking issues found. The implementation is consistent across all layers (Solidity → Rust coprocessor) and follows established patterns.


What was checked

Cross-layer consistency

  • Solidity FheType enum values (Uint8=2 through Uint128=6) match Rust integer type checks in check_fhe_operand_types and perform_fhe_operation_impl
  • Array size limits are consistent: 60 for Uint64/Uint128, 100 for Uint8–Uint32 in both Solidity (fheSum, checkHCUForFheSum) and Rust (FHE_SUM_MAX_INPUTS_WIDE/NARROW)
  • Protobuf common.proto correctly updated with FHE_SUM = 28, matching SupportedFheOperations::FheSum = 28
  • Handle encoding: _appendMetadataToPrehandle embeds type byte at position 30, consistent with get_ct_type reading handle[30] in Rust

Scheduler integration

  • output_type correctly extracted from node.result_handle via get_ct_type, threaded through run_computationperform_fhe_operationperform_fhe_operation_impl
  • Only consumed in FheSum branch; other operations ignore it — backward compatible

Event propagation

  • event_to_op_int correctly maps FheSum28
  • insert_computation receives variable-length dependencies via values.iter().collect()
  • tfhe_result_handle and tfhe_inputs_handle correctly updated

Edge cases

  • Empty arrays: Solidity allows it and charges HCU; Rust returns trivial encryption of zero via trivial_encrypt_be_bytes(output_type, &[0u8])
  • Single-element arrays: work correctly through standard sum path
  • Uninitialized values: FHE.sol library replaces with trivially encrypted zeros before passing to executor
  • Scalar rejection: FheSum rejects scalar inputs at both Solidity and Rust layers

Security

  • ACL checks properly enforced in _naryOp for each input handle
  • checkHCUForFheSum enforces msg.sender == fhevmExecutorAddress
  • Type consistency enforced: _typeOf(values[i]) != resultType check in _naryOp

Pattern consistency

  • New _naryOp helper appropriately abstracts the variadic ACL/type check pattern
  • collect_operands_as Rust helper cleanly extracts typed references
  • HCU bucketed pricing follows the established checkHCU* patterns
  • Version bumps consistent (FHEVMExecutor 0.3→0.4, HCULimit 0.2→0.3, reinitializer versions incremented)

Tests

  • Rust unit tests cover: op type, i16 roundtrip, empty/single/boundary inputs, type mismatch, scalar rejection
  • Solidity manual test suite covers all 5 FHE types, duplicate handles, uninitialized values, empty arrays, single elements
  • TypeScript coprocessorUtils.ts FheSum event handler correctly sums clear texts with modular arithmetic
  • Foundry tests updated for fheSum HCU validation

Nits (non-blocking)

  • Nit: The _naryOp function's IncompatibleTypes() error could include the index and actual/expected types for easier debugging of large arrays. The existing IncompatibleTypes error is used elsewhere without parameters, so this is consistent but could be enhanced.
  • Nit: The shift operator parenthesization changes in coprocessorUtils.ts (lines 17-23, 32-37) are correctness-preserving clarity improvements — % already has higher precedence than <</>> in JS. Good to make explicit regardless.

Reviewed with 4 parallel specialized agents: 2× bug hunter (opus), guideline compliance (sonnet), error handling auditor (sonnet).

@claude
Copy link
Copy Markdown

claude bot commented Apr 20, 2026

All four review agents have now completed. The review was already posted to PR #2274 with an approval. Here's the final tally:

Agent Model Result Duration
Bug Hunter 1 opus ✅ No bugs found ~2.4 min
Bug Hunter 2 opus ✅ No bugs found ~4.7 min
Guideline Compliance sonnet ✅ No violations ~0.6 min
Error Handling Auditor sonnet ⚠️ Suggestions only (pre-existing patterns) ~2.8 min

All agents converged on the same conclusion: this is a well-implemented, consistent end-to-end feature addition with no blocking issues.

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