Skip to content

Conversation

@mohammadfawaz
Copy link
Collaborator

Specifically,

  1. Introduce a PlaintextType::ExternalStruct, and allow referring to such a type in .aleo code by a Locator.

  2. Allow casting to external struct types.

  3. Let two struct types be considered the same type as long as they are structurally the same and have the same local names. This funky ruleset is effectively here for backwards compatibility. In more detail:

/// Equivalence of structs means they have the same local names (regardless of whether
/// they're local or external), and their members have the same names and equivalent
/// types in the same order, recursively.
///
/// Equivalence of arrays means they have the same length and their element types are
/// equivalent.
///
/// This definition of equivalence was chosen to balance these concerns:
///
/// 1. All programs from before the existence of external structs will continue to work -
/// thus it's necessary for a struct created from another program to be considered equivalent
/// to a local one with the same name and structure, as in practice that was the behavior.
/// 2. We don't want to allow a fork. Thus we do need to check names, not just structural
/// equivalence - otherwise we could get a program deployable to a node which is using
/// this check, but not deployable to a node running an earlier SnarkVM.

Also note this other usecase from a user:

currently we got in an issue where one mainnet deployed contract imports tokenRegistry, and the contract we are writing for A imports that contract and A contract. but, A contract and tokenRegistryContract have same struct names so we cannot compile that.If we cannot change struct name in A contract, we need to wait for snarkVM update, just to compile the contracts

Co-authored-by: Michael Benfield <mike.benfield@gmail.com>
@mohammadfawaz
Copy link
Collaborator Author

Rebase and squash #2798 and opened a new PR with Michael as the co-author of the single commit. This will make it easier to keep rebasing as we review this.

@mohammadfawaz
Copy link
Collaborator Author

@vicsn I added a test that uses an external struct inside a record: 4fb2cd3

- Add a few more tests
@mohammadfawaz mohammadfawaz force-pushed the struct-namespace-squashed branch from 71adc3d to 3e0e03c Compare November 28, 2025 19:25
@mohammadfawaz
Copy link
Collaborator Author

Review comments addressed in 3e0e03c

@mohammadfawaz mohammadfawaz requested a review from vicsn November 28, 2025 19:27
@vicsn
Copy link
Collaborator

vicsn commented Nov 28, 2025

Please no more force pushing after reviews have started

Copy link
Collaborator

@vicsn vicsn left a comment

Choose a reason for hiding this comment

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

LGTM, one test seems to fail. If you run with REWRITE_EXPECTATIONS=1, please still check the new output makes sense.

@vicsn vicsn requested a review from d0cd November 28, 2025 20:17
2 => Ok(Self::Array(ArrayType::read_le(&mut reader)?)),
3.. => Err(error(format!("Failed to deserialize annotation variant {variant}"))),
3 => Ok(Self::ExternalStruct(Locator::read_le(&mut reader)?)),
4.. => Err(error(format!("Failed to deserialize annotation variant {variant}"))),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Exists in current implementation, but it might be more clear to say

Suggested change
4.. => Err(error(format!("Failed to deserialize annotation variant {variant}"))),
4.. => Err(error(format!("Failed to deserialize plaintext type variant {variant}"))),

use crate::{ArrayType, Identifier, LiteralType, Locator, ProgramID, StructType};
use snarkvm_console_network::prelude::*;

/// A `PlaintextType` defines the type parameter for a literal, struct, or array.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// A `PlaintextType` defines the type parameter for a literal, struct, or array.
/// A `PlaintextType` defines the type parameter for a literal, struct, array, or external struct.

// Look up the struct
let struct_ = get_external_struct(locator)?;

// Account for the plaintext variant bits.
Copy link
Collaborator

@d0cd d0cd Dec 13, 2025

Choose a reason for hiding this comment

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

Could consider unifying with the struct logic in the variant above, for consistency.
Ditto for size_in_bits_raw.

}
}

for mapping in self.program.mappings().values() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This check is performed in verify.rs gated by a consensus version. Does it also need to happen here?


/// Returns whether this constructor refers to an external struct.
pub fn contains_external_struct(&self) -> bool {
self.commands.iter().any(|command| {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we also check the other command variants?

pub fn contains_external_struct(&self) -> bool {
self.commands
.iter()
.any(|command| matches!(command, Command::Instruction(inst) if inst.contains_external_struct()))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we also check the other command variants?

// Only cast instructions may contain an explicit reference to an external struct.
// Calls may produce them, but they don't explicitly reference the type, and that's
// always been allowed.
matches!(self,
Copy link
Collaborator

Choose a reason for hiding this comment

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

When a new instruction is added, we may forget to update this code.
For example, dynamic dispatch would in theory support get.dynamic.record r0.microcredits into r1 as foo.aleo/data, but it's easy for us to forget to update this rule.
This also holds true for some of the other checks that match on the instructions?
Is there a more reliable way we can enforce these properties?

// Cache the plaintext bits, and return the struct.
Ok(circuit::Plaintext::Struct(members, Default::default()))
}
PlaintextType::ExternalStruct(_identifier) => todo!(),
Copy link
Collaborator

@d0cd d0cd Dec 13, 2025

Choose a reason for hiding this comment

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

Bailing with an error message is fine if this is not supported.

// Cache the plaintext bits, and return the struct.
Ok(Plaintext::Struct(members, Default::default()))
}
PlaintextType::ExternalStruct(_identifier) => todo!(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Bailing with an error message is fine if this is not supported.

Ok(stack)
}

// ---------- 1️⃣ Literal equivalence ----------
Copy link
Collaborator

@d0cd d0cd Dec 13, 2025

Choose a reason for hiding this comment

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

Nit. We should get rid of the numerical emojis in this file, to support our friends whose editors can't render :)

}

// This test verifies that a program with external structs can be deployed on
// consensus version 12.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// consensus version 12.
// consensus version 13.

let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng);

// Initialize the VM at the correct height.
let v10_height = CurrentNetwork::CONSENSUS_HEIGHT(consensus_version).unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
let v10_height = CurrentNetwork::CONSENSUS_HEIGHT(consensus_version).unwrap();
let height = CurrentNetwork::CONSENSUS_HEIGHT(consensus_version).unwrap();


// Initialize the VM at the correct height.
let v10_height = CurrentNetwork::CONSENSUS_HEIGHT(consensus_version).unwrap();
let vm = crate::vm::test_helpers::sample_vm_at_height(v10_height, rng);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
let vm = crate::vm::test_helpers::sample_vm_at_height(v10_height, rng);
let vm = crate::vm::test_helpers::sample_vm_at_height(height, rng);

if consensus_version < ConsensusVersion::V13 {
ensure!(
!deployment.program().contains_external_struct(),
"Invalid deployment transaction '{id}' - external structs may only be used beginning with Consensus version 10"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
"Invalid deployment transaction '{id}' - external structs may only be used beginning with Consensus version 10"
"Invalid deployment transaction '{id}' - external structs may only be used beginning with `ConsensusVersion::V13`"


// Build two identical committee_state structs
cast r0 r1 into r2 as credits.aleo/committee_state;
cast r0 r1 into r3 as credits.aleo/committee_state;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you think it would be worthwhile to test two arrays one with external and one with local?

Copy link
Collaborator

@d0cd d0cd left a comment

Choose a reason for hiding this comment

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

It's getting close, I'm aligned with this notion of partial nominal and structural equivalence. Left a few suggestions.

A couple points/questions:

  • What is the import rule? If a program A uses an external struct in B which itself uses an external struct in C, does A have to import B and C? It would be great to document and test the rule.
  • Before merging, we should refresh the program regressions and run it to make sure that the VM can still be initialized with all of the new checks.

- Fix a bug related to type equivalence for nested external structs
@mohammadfawaz
Copy link
Collaborator Author

@d0cd I believe I addressed all your comments in 93e4e48 except for

This check is performed in verify.rs gated by a consensus version. Does it also need to happen here?

I'm not sure why the check is gated by a consensus version check in verify.rs. Do you remember why Michael added this? Also, if we do want to added a consensus version check in synthesizer/process/src/stack/mod.rs too, how do we go about that? How do we get the consensus version in that file?

What is the import rule? If a program A uses an external struct in B which itself uses an external struct in C, does A have to import B and C? It would be great to document and test the rule.

I added a test that showcases that transitive dependencies are not required at the top level as long as the top level does not explicitly use the external struct name directly. I think this rule matches how import is meant to work for all symbols.

Before merging, we should refresh the program regressions and run it to make sure that the VM can still be initialized with all of the new checks.

What does this mean exactly?

@vicsn
Copy link
Collaborator

vicsn commented Dec 17, 2025

I'm not sure why the check is gated by a consensus version check in verify.rs. Do you remember why Michael added this? Also, if we do want to added a consensus version check in synthesizer/process/src/stack/mod.rs too, how do we go about that? How do we get the consensus version in that file?

The consensus version check in verify.rs was added to ensure all validators agree external structs are only enabled from the new consensus version onwards. I think this comment's point is that the logic in synthesizer/process/src/stack/mod.rs might be duplicate and superfluous.

@d0cd
Copy link
Collaborator

d0cd commented Jan 4, 2026

Before merging, we should refresh the program regressions and run it to make sure that the VM can still be initialized with all of the new checks.

What does this mean exactly?

The regression repo is here.
It contains a few utilities we can run to make sure changes are backwards compatible.

@vicsn vicsn merged commit 718183c into staging Jan 7, 2026
6 of 7 checks passed
@vicsn vicsn deleted the struct-namespace-squashed branch January 7, 2026 21:08
@vicsn
Copy link
Collaborator

vicsn commented Jan 7, 2026

Thank you everyone :)

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.

4 participants