Skip to content

Fix synthesis of circuits with Scalar outputs#3258

Open
Antonio95 wants to merge 15 commits into
stagingfrom
fix/scalar-outputs
Open

Fix synthesis of circuits with Scalar outputs#3258
Antonio95 wants to merge 15 commits into
stagingfrom
fix/scalar-outputs

Conversation

@Antonio95

Copy link
Copy Markdown
Contributor

Closes #2801

Note: Before merging, confirm the merge CLI flow triggers synthesizer/tests/test_vm_execute_and_finalize.rs (this PR deletes the #[test] annotation preceding the #[test_log::test], which was causing duplicate execution of that long test).

The issue

Aleo programs which cast a value of type one of {Field, Group, Address} to Scalar and output the latter often cause errors during deployment verification.

The reason is that (unlike a console::Scalar) the circuit representation of a Scalar actually contains a Field element, which has ~2 more bits (i.e. takes values up to ~4 times larger) than a Scalar. When a function’s circuit is constructed (e.g. during key synthesis and verifying-key verification), the resulting Response is ejected, which in particular ejects the outputs therein. Since inputs to the function are sampled randomly during synthesis, an input of one of the three types mentioned at the start and cast to a Scalar which is subsequently output often results in an underlying Field value which panics at eject time.

Only this case of cast is problematic. For instance, when casting U16 to U8, constraints are added to the circuit to ensure the most significant 8 bits are 0, and only the 8 least significant bits form the new U8 value. Constraints are not enforced during key synthesis, and ejecting the 8 bits of the U8 always results in valid console::U8.

There are two subtler places where problematic Scalars were being ejected other than when ejecting the response: in the load_circuit and store_circuit calls in the the circuit-construction part of the cast instruction. These two functions perform a type check of the circuit value received against the destination register’s type. This was done by first ejecting the circuit value into a console one and then calling StackTrait’s matches_value_type (which only accepts console types). This ejection also resulted in an error once the aforementioned one was fixed.

The fix

Part of the design philosophy has been not to add any special-case treatment to Scalar type for improved future maintainability.

  • In CheckDeployment and Synthesize mode, we no longer eject the Response at the point where we were. Instead of returning that, we now return an Option which is only populated in modes other than the two above, returning None in those two.
  • At subsequent points down the execution flow, in CheckDeployment and Synthesize mode, we sample random outputs. These are the only part of the (no longer present) Response needed at that stage, and only their type is relevant.
  • The eject + matches_value_type calls in load_circuit and store_circuit have been replaced by a new circuit_matches_value_type. This function performs the same type exploration as matches_value_type but acts on circuit values and does not eject. Although this results in some code duplication, it does not result in a performance loss but the opposite: the eject + matches_value_type flow effectively resulted in two separate explorations of the same type tree (plus any relevant value conversions in eject), as opposed to the single exploration in the new circuit_matches_value_type (and no value conversions).

The test file synthesizer/src/vm/tests/test_v15/scalar_outputs.rs has been added, which tests key synthesis and verification in functions with previously failing casts. This has been checked on the three problematic source types, both for private and public inputs and outputs, as well as in closures.

Note this PR does not require a new ConsensusVersion. Expectations of test_vm_execute_and_finalize had to be regenerated since the sampling of random request outputs (where before a Request was being ejected instead) desynchronised the state of the rng with respect to pre-change executions and resulted in outputs different than expected. The expectation regeneration was inspected to be correct (and not, e.g., overwrite previously-expected-to-fail cases with passing ones or vice-versa).

@Antonio95 Antonio95 requested review from Copilot and vicsn May 13, 2026 14:40

Copilot AI left a comment

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.

Pull request overview

This PR addresses deployment verification failures caused by ejecting circuit Scalar outputs (notably when casting from Field/Group/Address to Scalar) during Synthesize and CheckDeployment flows. It does so by avoiding response ejection in those modes and adding circuit-native type checking for register values, with regenerated integration-test expectations to reflect RNG consumption changes.

Changes:

  • Change Stack::execute_function to return Option<Response> and skip ejecting a Response in Synthesize/CheckDeployment, sampling dummy outputs downstream where only types are needed.
  • Replace console-ejection-based type checks in load_circuit/store_circuit with a new circuit_matches_register_type circuit-native type checker.
  • Add a regression test for scalar outputs and update execute_and_finalize expectations; adjust the long integration test to avoid duplicate execution.

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
vm/package/run.rs Adapts execute_function call site to new Option<Response> return type.
synthesizer/tests/test_vm_execute_and_finalize.rs Removes duplicate #[test] attribute and adds per-test logging.
synthesizer/tests/expectations/vm/execute_and_finalize/user_callable.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/public_wallet.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/program_callable.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/nested_external_struct.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/multi_external_struct.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/many_input_and_output.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/interleave_async_and_non_async.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/future_with_external_struct.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/future_with_external_struct_complex.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/future_out_of_order.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/external_struct.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_record.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_external_struct.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/external_struct_in_external_record.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/external_future_arg_access.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/count_usages.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/complex_finalization.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/child_and_parent.out Regenerated expectations due to RNG/outputs changes.
synthesizer/tests/expectations/vm/execute_and_finalize/branch_with_future.out Regenerated expectations due to RNG/outputs changes.
synthesizer/src/vm/verify.rs Cleans up test-module imports.
synthesizer/src/vm/tests/test_v15/scalar_outputs.rs Adds regression test covering scalar outputs from problematic casts.
synthesizer/src/vm/tests/test_v15/mod.rs Registers the new scalar output test module.
synthesizer/program/src/traits/stack_and_registers.rs Adds circuit_matches_register_type to the RegistersCircuit trait.
synthesizer/process/src/tests/test_execute.rs Updates test to handle Option<Response> from execute_function.
synthesizer/process/src/stack/registers/registers_circuit.rs Implements circuit-native type checking to avoid ejection in circuit paths.
synthesizer/process/src/stack/registers/mod.rs Updates imports related to the registers module.
synthesizer/process/src/stack/execute.rs Changes execute_function signature to Result<Option<Response<_>>> and gates response ejection by mode.
synthesizer/process/src/stack/call/standard.rs Ensures Synthesize/CheckDeployment paths operate without a console response and sample dummy outputs when needed.
synthesizer/process/src/stack/call/dynamic.rs Updates dynamic-call execution to unwrap Option<Response> appropriately by mode.
synthesizer/process/src/execute.rs Updates process execution to require a populated response in Execute mode.
circuit/program/src/data/future/mod.rs Adds Future::arguments() accessor used by circuit-side type checking.
.circleci/config.yml Adds a feature branch to the merge-workflow branch filter.

Comment thread .circleci/config.yml
Comment thread synthesizer/src/vm/tests/test_v15/scalar_outputs.rs
use console::{
network::prelude::*,
program::{Entry, Literal, Plaintext, Register, Request, Value},
program::{Entry, Literal, Plaintext, PlaintextType, Register, RegisterType, Request, Value},

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.

They are used in the child module registers_circuit.rs, which imports super::*, which is why clippy was happy. I've now moved the two imports to the child module anyway.

@Antonio95 Antonio95 marked this pull request as draft May 13, 2026 15:42
Comment thread synthesizer/process/src/stack/registers/registers_circuit.rs
@Antonio95 Antonio95 changed the title fixed issue, regenerated expecations Fix synthesis of circuits with scalar outputs May 14, 2026
@Antonio95 Antonio95 changed the title Fix synthesis of circuits with scalar outputs Fix synthesis of circuits with Scalar outputs May 14, 2026

@vicsn vicsn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You may have forgotten to commit the new macro

Comment thread synthesizer/process/src/stack/call/standard.rs Outdated
Comment thread synthesizer/src/vm/mod.rs Outdated
assert_eq!(
deployment_transaction_ids,
vec![deployment_1.id(), deployment_2.id(), deployment_4.id(), deployment_3.id()],
vec![deployment_4.id(), deployment_1.id(), deployment_3.id(), deployment_2.id()],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can you remove this assert? I've thought for years its useless and creates busywork to just update the expectation

@Antonio95 Antonio95 May 15, 2026

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.

@vicsn I think part of what that test wants to check is that VM::from imports programs (i.p. depending on one another) correctly regardless of the order vm.transaction_store() returns them in. So, in order for the check assert!(VM::from(vm.store.clone()).is_ok()); in line 1289 to be meaningful, one has to check vm.transaction_store() is not already returning them in the correct import order (1, 2, 3, 4) by chance.

Which of the two following options do you favour?

  • Replacing the concrete hardcoded order in the check to the check "make sure vm.transaction_store() returns them in an order different from 1, 2, 3, 4". This would maintain the meaningfulness of the test and make it so that, on average, if any PR changes things that affect that order (i.p. the random transaction IDs), this test will need to be updated with probability only 1 / 4! = 1/24 (= the chance that the random changes result in the order 1, 2, 3, 4).
  • Removing this check and living with the risk of the test not being meaningful during some periods in the future. This doesn't seem too bad in the sense that this has been tested over and over, but who knows if future changes could potentially break it (in which case it's likely other tests would break anyway).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Removing is fine

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.

Done 👌

Comment thread synthesizer/process/src/stack/registers/registers_circuit.rs

@Antonio95 Antonio95 left a comment

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.

Double checked everything.

@Antonio95 Antonio95 marked this pull request as ready for review May 15, 2026 08:43
@Antonio95

Copy link
Copy Markdown
Contributor Author

(All flows pass with the exception of the exact same test cases which are already failing in staging)

@Antonio95 Antonio95 force-pushed the fix/scalar-outputs branch from 2202627 to f909e5f Compare May 15, 2026 09:17
@vicsn vicsn requested a review from mohammadfawaz May 15, 2026 09:30
@mohammadfawaz

Copy link
Copy Markdown
Collaborator

If I understand correctly, this fixes a class of programs that didn't use to deploy and now does. Can you explain further why this doesn't need a consensus version check? Are we worried about a window where some nodes have upgraded and others haven't?


// Tests that a function which receives a Field/Group/Address input, casts it to
// a Scalar and outputs the latter, deploys correctly.
#[test]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A few extra test suggestions:

  • Cross-function call returning a scalar: function A calls function B where B outputs a scalar (call.dynamic too).
  • Nested scalar (struct/array/record entry).
  • Negative test that the new circuit_matches_value_type still rejects a type-mismatched value.
  • Test execution here, not just deployment.

register_type: &RegisterType<N>,
) -> Result<()> {

let value_kind = match value {

@mohammadfawaz mohammadfawaz Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

value_kind is only used in the final bail! arm but it's built on every call. Maybe move its definition into the _ => arm?

let transition = Transition::from(&console_request, &response, &output_types, &output_registers)?;
let transition = Transition::from(
&console_request,
console_response.as_ref().unwrap(),

@mohammadfawaz mohammadfawaz Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe justify why this unwrap is safe (an expect maybe)

let transition = Transition::from(&console_request, &response, &output_types, &output_registers)?;
let transition = Transition::from(
&console_request,
console_response.as_ref().unwrap(),

@mohammadfawaz mohammadfawaz Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here. expect instead of unwrap?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Deployment can fail depending on sampled values

4 participants