Skip to content

Conversation

@d0cd
Copy link
Collaborator

@d0cd d0cd commented Jan 28, 2026

Motivation

The existing Identifier type has value-dependent bit representation in circuits—its to_bits output length varies based on the identifier's string length. This makes it unsuitable as a literal type where deterministic, fixed-size circuit behavior is required for consistent constraint counts across all valid inputs.

This PR introduces IdentifierLiteral, a new literal type with a fixed 31-byte (248-bit) representation that can be used safely in circuits.

What's New

Type: IdentifierLiteral<E> / identifier

  • Fixed 31-byte representation with null-padded trailing bytes
  • Syntax: 'identifier_name' (single-quoted, no type suffix)
  • Character set: [a-zA-Z][a-zA-Z0-9_]* (must start with a letter)
  • Packs into a single field element

Console Layer (console/types/string/src/identifier_literal/):

  • Parsing, serialization, ToBits, FromBits, ToField, FromField
  • Validation of character constraints and trailing-null invariant

Circuit Layer (circuit/types/string/src/identifier_literal/):

  • Bit-level validation via constraints (ASCII high-bit, character categories, first-char letter, trailing nulls)
  • ~1275 constraints for non-constant mode validation

Integration:

  • Extended Literal enum with Identifier variant
  • Added LiteralType::Identifier and PlaintextType support
  • Cast operations: identifier to/from field (and through field to other types)
  • Blocked for rand.chacha and hash destination types (cannot generate random identifiers)

Usage in Aleo Programs

Declaring inputs/outputs:

program example.aleo;

function greet:
    input r0 as identifier.public;
    is.eq r0 'hello' into r1;
    output r1 as boolean.public;

Using literals in instructions:

function check_name:
    input r0 as identifier.private;
    is.eq r0 'alice' into r1;      // Compare against literal
    cast 'bob' into r2 as field;   // Cast literal to field
    output r1 as boolean.private;

Passing as input (CLI/SDK):

# As a program input
snarkvm execute example.aleo greet "'hello'"

# In JSON input format
{ "r0": "'my_identifier'" }

In structs and mappings:

struct Config:
    name as identifier;
    value as u64;

mapping configs:
    key as identifier.public;
    value as Config.public;

Test Plan

Unit Tests (72 total)

Console layer (cargo test -p snarkvm-console-types-string --lib identifier_literal):

  • 36 tests covering parsing, serialization, bit/field conversions, random sampling
  • Exhaustive validation of all 256 byte values for first and subsequent character positions
  • Round-trip tests for all conversion paths
  • Invalid input rejection (non-ASCII, invalid chars, non-trailing nulls, empty, oversized)

Circuit layer (cargo test -p snarkvm-circuit-types-string --lib identifier_literal):

  • 36 tests covering injection, equality, bit/field conversions
  • Circuit constraint count verification across Constant/Public/Private modes
  • Unsatisfied circuit tests for invalid inputs (malformed field values)

Integration Test

synthesizer/src/vm/tests/test_v14.rs::test_identifier_literal_migration:

  • Verifies deployment blocked before ConsensusVersion::V14
  • Verifies deployment succeeds after V14 activation
  • Executes function with identifier literal input

Documentation

The following documentation should be updated:

  • Aleo Language Specification: Add identifier to the list of literal types with syntax 'name'
  • Type Reference: Document the 31-byte limit and valid character set [a-zA-Z][a-zA-Z0-9_]*
  • SDK/CLI Examples: Show how to pass identifier literals as program inputs

Backwards Compatibility

Consensus Gating

This feature is gated behind ConsensusVersion::V14:

  1. Syntax Detection: contains_v14_syntax() extended to detect:

    • Operand::Literal where literal type is Identifier
    • identifier type in function/closure inputs and outputs
    • identifier type in finalize inputs
    • identifier type in struct members, record entries, and mapping keys/values
  2. Keyword Restriction: "identifier" added to RESTRICTED_KEYWORDS for V14+, preventing its use as a user-defined identifier in programs deployed at or after V14.

  3. Deployment Validation: Programs using identifier literals or the identifier type will fail deployment before the V14 activation height.

No Breaking Changes

  • Existing programs are unaffected
  • No changes to consensus rules for pre-V14 blocks
  • The new type only becomes available after V14 activation

@d0cd d0cd requested a review from vicsn January 30, 2026 23:38
/// Reads the identifier literal from a buffer.
#[inline]
fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
// Read the number of content bytes.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Version byte?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Existing Identifer doesn't have a version byte (link[https://github.com/ProvableHQ/snarkVM/blob/f8fd99469e73b30638f3f6b0cec461f58a5ea9cd/console/program/src/data/identifier/bytes.rs#L20]) and neither do most Literal variants.
Do we still want to throw it in?

@d0cd d0cd requested a review from Antonio95 February 5, 2026 07:10
Copy link
Contributor

@Antonio95 Antonio95 left a comment

Choose a reason for hiding this comment

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

Very cool feature. I've reviewed it paying special attention to the in-circuit checks of byte correctness (which are quite complex and seem correct to me). Left only a couple of minor comments, none of them critical.

@raychu86
Copy link
Collaborator

raychu86 commented Feb 5, 2026

ProgramID's are all lowercase alphanumeric (is_lowercase_alphanumeric()). Does this affect the casting into program usage for dynamic dispatch?

@d0cd
Copy link
Collaborator Author

d0cd commented Feb 5, 2026

ProgramID's are all lowercase alphanumeric (is_lowercase_alphanumeric()). Does this affect the casting into program usage for dynamic dispatch?

It does not. The target of a dynamic call is resolved here.
If a ProgramID fails to be constructed or does not exist, then the instruction fails.
The verifier must also input a correct program ID here.

@vicsn vicsn requested review from Antonio95 and raychu86 February 9, 2026 11:08
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