Skip to content

fix: Improve Steel IDL detection for 1-byte discriminator arrays#62

Merged
adiman9 merged 1 commit intomainfrom
fixes
Mar 19, 2026
Merged

fix: Improve Steel IDL detection for 1-byte discriminator arrays#62
adiman9 merged 1 commit intomainfrom
fixes

Conversation

@adiman9
Copy link
Contributor

@adiman9 adiman9 commented Mar 19, 2026

No description provided.

@vercel
Copy link

vercel bot commented Mar 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperstack-docs Ready Ready Preview, Comment Mar 19, 2026 10:24am

Request Review

@greptile-apps
Copy link

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR extends the Steel IDL detection heuristic in IdlSnapshot::deserialize to handle a second variant where the discriminant is stored as a 1-byte discriminator JSON array (rather than via a dedicated discriminant field). This covers a specific code-generation path where a Steel IDL serialises its u8 discriminant directly into the discriminator field.

  • The new is_steel_short_discriminator condition (!has_discriminant && disc_len == 1) is logically sound: Anchor always uses 8-byte discriminators, so a 1-byte array strongly indicates Steel.
  • get_discriminator() already handles this case correctly — a non-empty self.discriminator is returned as-is, yielding the expected 1-byte vec.
  • No unit test is added for the new code path; the existing test_snapshot_serde round-trip test uses a fixed 8-byte discriminator and does not exercise either Steel detection variant through raw JSON.
  • The interaction between variant 1 and variant 2 in mixed-instruction programs (where some instructions use discriminant and others use a 1-byte discriminator) resolves safely through .all(), but this is undocumented and non-obvious.

Confidence Score: 4/5

  • Safe to merge for Steel IDL use cases; the heuristic is correct for standard formats but the new path is untested.
  • The logic change is small, well-reasoned, and consistent with how get_discriminator() already works. The main gap is the absence of a regression test for the new detection variant, which means future refactors could silently break it. There are no runtime errors, security issues, or data-corruption risks in the change itself.
  • hyperstack-idl/src/snapshot.rs — the new is_steel_short_discriminator branch has no corresponding unit test.

Important Files Changed

Filename Overview
hyperstack-idl/src/snapshot.rs Adds a second Steel IDL detection variant (1-byte discriminator array, no discriminant field) to the discriminant_size heuristic; the logic is sound for standard Anchor/Steel formats, but lacks a unit test for the new path and the interaction with mixed-variant programs is undocumented.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Deserialize JSON to serde_json::Value] --> B{instructions array present\nand non-empty?}
    B -- No --> Z[discriminant_size = 8\nDefault / Anchor]
    B -- Yes --> C[For each instruction .all]

    C --> D{has 'discriminant' key?}
    D -- Yes --> E{has non-empty 'discriminator'?}
    E -- No --> F[is_steel_discriminant = true\nVariant 1 - explicit discriminant field]
    E -- Yes --> G[is_steel_discriminant = false]

    D -- No --> H{disc_len == 1?}
    H -- Yes --> I[is_steel_short_discriminator = true\nVariant 2 - 1-byte array NEW]
    H -- No --> J[is_steel_short_discriminator = false]

    F --> K{is_steel_discriminant OR\nis_steel_short_discriminator?}
    G --> K
    I --> K
    J --> K

    K -- all true --> L[discriminant_size = 1\nSteel-style]
    K -- any false --> Z
Loading

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: hyperstack-idl/src/snapshot.rs
Line: 56-62

Comment:
**No tests added for new detection variant**

The new `is_steel_short_discriminator` branch is the only untested code path in this file. The existing test in `test_snapshot_serde` uses an 8-byte discriminator and a `null` discriminant, which exercises neither variant 1 nor variant 2 of the Steel detection through the custom `Deserialize` impl (since the round-trip test bypasses raw JSON parsing of this heuristic).

A dedicated test like the following should be added to verify that the detection works end-to-end:

```rust
#[test]
fn test_steel_short_discriminator_detection() {
    let json = r#"{
        "name": "test_program",
        "version": "0.1.0",
        "accounts": [],
        "instructions": [
            {
                "name": "my_ix",
                "discriminator": [42],
                "accounts": [],
                "args": []
            }
        ],
        "types": [],
        "events": [],
        "errors": []
    }"#;
    let snapshot: IdlSnapshot = serde_json::from_str(json).expect("deserialize");
    assert_eq!(snapshot.discriminant_size, 1);
}
```

Without this, a regression that breaks variant 2 detection would go unnoticed.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: hyperstack-idl/src/snapshot.rs
Line: 60

Comment:
**Heuristic may conflict with `get_discriminator()` for 1-byte arrays**

When `is_steel_short_discriminator` is true, `discriminant_size` is set to `1`. Inside `get_discriminator()` (line 187), if `self.discriminator` is non-empty (e.g., `[42]`), it is returned as-is — so the returned `Vec<u8>` has length 1.

However, if any downstream consumer validates `get_discriminator().len() == discriminant_size` or slices transaction bytes using `discriminant_size`, the 1-byte vec returned for variant 2 is consistent with `discriminant_size == 1`. This is fine as long as all consumers are aligned.

The subtle risk is: if a program has a mix of instructions — some satisfying variant 1 (`is_steel_discriminant`) and others variant 2 (`is_steel_short_discriminator`) — the `.all()` still resolves to `true` and `discriminant_size` becomes `1`. In that case, variant-1 instructions (which have no `discriminator` array at all, so `self.discriminator` is empty) fall through to `get_discriminator()`'s `discriminant` branch and return a 1-byte vec, which remains consistent. Mixed programs appear safe, but the interaction is non-obvious and worth a comment.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix: Improve Steel I..."

Comment on lines +56 to +62
// Steel-style variant 2: discriminator is stored as a 1-byte array (no
// discriminant field). This happens when the AST was generated from a
// Steel IDL that serialised its u8 discriminant directly into the
// discriminator field.
let is_steel_short_discriminator = !has_discriminant && disc_len == 1;

is_steel_discriminant || is_steel_short_discriminator
Copy link

Choose a reason for hiding this comment

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

P2 No tests added for new detection variant

The new is_steel_short_discriminator branch is the only untested code path in this file. The existing test in test_snapshot_serde uses an 8-byte discriminator and a null discriminant, which exercises neither variant 1 nor variant 2 of the Steel detection through the custom Deserialize impl (since the round-trip test bypasses raw JSON parsing of this heuristic).

A dedicated test like the following should be added to verify that the detection works end-to-end:

#[test]
fn test_steel_short_discriminator_detection() {
    let json = r#"{
        "name": "test_program",
        "version": "0.1.0",
        "accounts": [],
        "instructions": [
            {
                "name": "my_ix",
                "discriminator": [42],
                "accounts": [],
                "args": []
            }
        ],
        "types": [],
        "events": [],
        "errors": []
    }"#;
    let snapshot: IdlSnapshot = serde_json::from_str(json).expect("deserialize");
    assert_eq!(snapshot.discriminant_size, 1);
}

Without this, a regression that breaks variant 2 detection would go unnoticed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: hyperstack-idl/src/snapshot.rs
Line: 56-62

Comment:
**No tests added for new detection variant**

The new `is_steel_short_discriminator` branch is the only untested code path in this file. The existing test in `test_snapshot_serde` uses an 8-byte discriminator and a `null` discriminant, which exercises neither variant 1 nor variant 2 of the Steel detection through the custom `Deserialize` impl (since the round-trip test bypasses raw JSON parsing of this heuristic).

A dedicated test like the following should be added to verify that the detection works end-to-end:

```rust
#[test]
fn test_steel_short_discriminator_detection() {
    let json = r#"{
        "name": "test_program",
        "version": "0.1.0",
        "accounts": [],
        "instructions": [
            {
                "name": "my_ix",
                "discriminator": [42],
                "accounts": [],
                "args": []
            }
        ],
        "types": [],
        "events": [],
        "errors": []
    }"#;
    let snapshot: IdlSnapshot = serde_json::from_str(json).expect("deserialize");
    assert_eq!(snapshot.discriminant_size, 1);
}
```

Without this, a regression that breaks variant 2 detection would go unnoticed.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

// discriminant field). This happens when the AST was generated from a
// Steel IDL that serialised its u8 discriminant directly into the
// discriminator field.
let is_steel_short_discriminator = !has_discriminant && disc_len == 1;
Copy link

Choose a reason for hiding this comment

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

P2 Heuristic may conflict with get_discriminator() for 1-byte arrays

When is_steel_short_discriminator is true, discriminant_size is set to 1. Inside get_discriminator() (line 187), if self.discriminator is non-empty (e.g., [42]), it is returned as-is — so the returned Vec<u8> has length 1.

However, if any downstream consumer validates get_discriminator().len() == discriminant_size or slices transaction bytes using discriminant_size, the 1-byte vec returned for variant 2 is consistent with discriminant_size == 1. This is fine as long as all consumers are aligned.

The subtle risk is: if a program has a mix of instructions — some satisfying variant 1 (is_steel_discriminant) and others variant 2 (is_steel_short_discriminator) — the .all() still resolves to true and discriminant_size becomes 1. In that case, variant-1 instructions (which have no discriminator array at all, so self.discriminator is empty) fall through to get_discriminator()'s discriminant branch and return a 1-byte vec, which remains consistent. Mixed programs appear safe, but the interaction is non-obvious and worth a comment.

Prompt To Fix With AI
This is a comment left during a code review.
Path: hyperstack-idl/src/snapshot.rs
Line: 60

Comment:
**Heuristic may conflict with `get_discriminator()` for 1-byte arrays**

When `is_steel_short_discriminator` is true, `discriminant_size` is set to `1`. Inside `get_discriminator()` (line 187), if `self.discriminator` is non-empty (e.g., `[42]`), it is returned as-is — so the returned `Vec<u8>` has length 1.

However, if any downstream consumer validates `get_discriminator().len() == discriminant_size` or slices transaction bytes using `discriminant_size`, the 1-byte vec returned for variant 2 is consistent with `discriminant_size == 1`. This is fine as long as all consumers are aligned.

The subtle risk is: if a program has a mix of instructions — some satisfying variant 1 (`is_steel_discriminant`) and others variant 2 (`is_steel_short_discriminator`) — the `.all()` still resolves to `true` and `discriminant_size` becomes `1`. In that case, variant-1 instructions (which have no `discriminator` array at all, so `self.discriminator` is empty) fall through to `get_discriminator()`'s `discriminant` branch and return a 1-byte vec, which remains consistent. Mixed programs appear safe, but the interaction is non-obvious and worth a comment.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

@adiman9 adiman9 merged commit 3fcd1ee into main Mar 19, 2026
10 checks passed
@adiman9 adiman9 deleted the fixes branch March 19, 2026 10:30
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.

1 participant