Skip to content

Conversation

@0xLeo-sqds
Copy link
Collaborator

Implement dual-format support for creating program interaction policies:

  • Legacy format (V1): Full Pubkeys embedded in all structures
  • New format (V2): Pubkey deduplication table with index references

Changes:

  • Add indexed constraint structures (InstructionConstraintIndexed, HookIndexed,
    AccountConstraintIndexed, AccountConstraintTypeIndexed)
  • Implement efficient index → Pubkey expansion during policy creation
  • Support up to 240 unique pubkeys per policy (indices 240-255 reserved for
    future builtin programs)
  • Add ProgramInteractionInvalidPubkeyTableIndex and
    ProgramInteractionTooManyUniquePubkeys errors
  • Update PolicyCreationPayload enum with LegacyProgramInteraction and
    ProgramInteraction variants
  • Both formats convert to same on-chain state (full Pubkeys)

Benefits:

  • Wire format optimization: 32 bytes → 1 byte per pubkey reference
  • Backward compatible: Legacy format still supported

@0xLeo-sqds
Copy link
Collaborator Author

0xLeo-sqds commented Nov 26, 2025

Additional comments worthy of discussion:

Edit: Comments discussed, no additional change to perform

Issue: Unbounded Constraint Complexity

I identified a potential improvement that applies to both the legacy and new indexed formats.

Currently, there are no limits on:

  • Number of account_constraints per InstructionConstraint;
  • Number of pubkeys within a single AccountConstraintType::Pubkey constraint;

Example of potentially expensive policy:

InstructionConstraint {
    account_constraints: vec![
        AccountConstraint {
            account_constraint: Pubkey(vec![pubkey1, pubkey2, ..., pubkey50])
        },
        // ... repeated 20 times
    ]
}

I know this is actually bound by tx size so a policy like the one above it's not possible, but still during constraint evaluation, each pubkey list is checked via linear scan (.contains()) meaning that a lower amount than that still generates some inefficiencies.

We could decide to add a per-constraint complexity limits during policy creation:

// Constants at module level
const MAX_ACCOUNT_CONSTRAINTS_PER_INSTRUCTION: usize = 20;
const MAX_PUBKEYS_PER_ACCOUNT_CONSTRAINT: usize = 10;

impl PolicyPayloadConversionTrait for ProgramInteractionPolicyCreationPayload {
    fn to_policy_state(self) -> Result<ProgramInteractionPolicy> {
        // ... existing validations ...

        // Validate constraint complexity
        for ic in &self.instructions_constraints {
            require!(
                ic.account_constraints.len() <= MAX_ACCOUNT_CONSTRAINTS_PER_INSTRUCTION,
                SmartAccountError::ProgramInteractionTooManyAccountConstraints
            );
            
            for ac in &ic.account_constraints {
                if let AccountConstraintTypeIndexed::Pubkey(indices) = &ac.account_constraint {
                    require!(
                        indices.len() <= MAX_PUBKEYS_PER_ACCOUNT_CONSTRAINT,
                        SmartAccountError::ProgramInteractionTooManyPubkeysInSingleConstraint
                    );
                }
            }
        }

        // ... rest of expansion logic ...
    }
}

Issue: Duplicate Pubkeys in Constraints

The current implementation (in both legacy and indexed formats) allows duplicate pubkeys within a single constraint:

  • Legacy format: AccountConstraintType::Pubkey(vec![TOKEN_PROGRAM, TOKEN_PROGRAM, SYSTEM_PROGRAM])
  • Indexed format: AccountConstraintTypeIndexed::Pubkey(vec![0, 0, 2])

Both expand to constraints with duplicate pubkeys in the final policy state, which wastes space and compute.~~

@0xLeo-sqds 0xLeo-sqds marked this pull request as ready for review November 26, 2025 11:23
@0xLeo-sqds
Copy link
Collaborator Author

0xLeo-sqds commented Nov 26, 2025

To make the SmallVec type more ergonomic and a zero-cost abstraction wrapper (Vec::From() creates unnecessary allocations/copies if used with .iter() like in our case) I added the Deref trait.

impl<L, T> Deref for SmallVec<L, T> {
    type Target = Vec<T>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

This shouldn't break any existing code

@socket-security
Copy link

socket-security bot commented Nov 27, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​@​solana/​spl-memo@​0.2.3 ⏵ 0.2.598 +410083 +280 +4100
Addednpm/​@​solana/​spl-token@​0.3.610010010083100
Addednpm/​typescript@​4.9.41001009010090

View full report

@socket-security
Copy link

socket-security bot commented Nov 27, 2025

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
License policy violation: npm typescript under MIT-Khronos-old

License: MIT-Khronos-old - the applicable license policy does not allow this license (4) (package/ThirdPartyNoticeText.txt)

License: CC-BY-4.0 - the applicable license policy does not allow this license (4) (package/ThirdPartyNoticeText.txt)

License: LicenseRef-W3C-Community-Final-Specification-Agreement - the applicable license policy does not allow this license (4) (package/ThirdPartyNoticeText.txt)

From: package.jsonnpm/[email protected]

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@0xLeo-sqds
Copy link
Collaborator Author

SmallVec Serialization Fix

The Problem

Solita generates beet.array() for all array types, which uses a 4-byte length prefix. However, Rust's SmallVec<u8, T> uses a 1-byte prefix and SmallVec<u16, T> uses a 2-byte prefix.

This caused serialization mismatches between the SDK and on-chain program. Previously, we worked around this by manually defining these types in types.ts and excluding them from solita generation (via .solitarc.js ignoredTypes).

This would have required a big lift for this PR since all nested types uses SmallVec.

The Solution

Added scripts/fix-smallvec.js: a post-processing script that automatically fixes the generated files after solita runs.

It replaces beet.array(X) with smallArray(beet.u8, X) or smallArray(beet.u16, X) as needed.

Usage: Runs automatically via yarn generate (or manually with node scripts/fix-smallvec.js)

What Changed

  • Removed from ignoredTypes: TransactionMessage, CompiledInstruction, MessageAddressTableLookup since these are now generated and post-processed
  • New generated types: All the *Compiled types for the new pubkeyTable format (InstructionConstraintCompiled, HookCompiled, AccountConstraintCompiled, etc.)

@0xLeo-sqds 0xLeo-sqds merged commit ee728b5 into policies Dec 17, 2025
2 checks passed
@0xLeo-sqds 0xLeo-sqds deleted the create-program-interaction-improvement branch December 17, 2025 08:34
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