Skip to content

feat: Add DelegateWithActions#146

Merged
snawaz merged 24 commits intomainfrom
snawaz/delegate-with-actions
Mar 12, 2026
Merged

feat: Add DelegateWithActions#146
snawaz merged 24 commits intomainfrom
snawaz/delegate-with-actions

Conversation

@snawaz
Copy link
Contributor

@snawaz snawaz commented Feb 20, 2026

It's the first version of DelegateWithActions instruction, along with following:

  • offchain and onchain APIs to create PostDelegationInstruction and PostDelegationActions.
  • Split the program crate, extracting out dlp-api that contains encryption and the instruction builders.
  • It is far from being perfect. Lots of improvements, and error propagations are possible and must be done as soon as the entire e2e flow works!

Summary by CodeRabbit

Release Notes

  • New Features

    • Added delegate_with_actions instruction enabling delegated accounts to execute post-delegation actions with optional encryption support.
    • Introduced encryption/decryption framework for protecting delegated action payloads.
    • Implemented compact instruction encoding for efficient on-chain representation.
  • Infrastructure

    • Reorganized instruction builders into dedicated API package.
    • Added comprehensive automated code analysis and linting configuration.
    • Enhanced feature flag organization for encryption and processing capabilities.

Copy link
Contributor Author

snawaz commented Feb 20, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@snawaz snawaz force-pushed the snawaz/delegate-with-actions branch 11 times, most recently from 1ab7892 to cbf6d6e Compare February 24, 2026 10:35
@snawaz snawaz marked this pull request as ready for review February 24, 2026 10:35
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/run-tests.yml (1)

47-56: ⚠️ Potential issue | 🔴 Critical

Missing nightly toolchain installation — cargo +nightly fmt will fail.

The lint job does not install the nightly toolchain. cargo +nightly fmt requires rustup to have the nightly toolchain available, but this job only has stable pre-installed on ubuntu-latest. The install job's toolchain setup doesn't carry over since GitHub Actions jobs run on separate runners.

🐛 Proposed fix: install nightly in the lint job
  lint:
    needs: install
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
+     - name: install nightly toolchain
+       uses: dtolnay/rust-toolchain@nightly
+       with:
+         components: rustfmt
      - name: Run fmt
        run: cargo +nightly fmt -- --check
      - name: Run clippy
        run: cargo clippy -- --deny=warnings
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/run-tests.yml around lines 47 - 56, The lint job currently
runs "cargo +nightly fmt -- --check" but does not install the nightly toolchain
on the runner; update the "lint" job to install and activate the nightly
toolchain before running cargo commands (for example run a step that installs
rustup/toolchain and executes "rustup toolchain install nightly" and/or "rustup
default nightly" or use the actions-rs/toolchain action) so that "cargo +nightly
fmt -- --check" and "cargo clippy -- --deny=warnings" run successfully.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Around line 70-75: The Cargo.toml currently pins serde exactly with serde =
"=1.0.226" which is too restrictive; change that dependency declaration (the
serde line) to either a caret range to allow compatible patch updates (serde =
"^1.0.226") or update it to the newer available release (serde = "1.0.228") so
transitive requirements from solana-address and solana-sdk can resolve without
forcing an exact version.

In `@src/args/delegate.rs`:
- Around line 2-7: The compile error comes from deriving Serialize/Deserialize
for a struct (e.g., DelegateArgs) that includes solana_program::pubkey::Pubkey
which needs the serde feature; update the solana-program dependency in
Cargo.toml to enable serde (for example set solana-program = { version =
">=1.16, <3.0.0", features = ["serde"] }) so Pubkey implements
Serialize/Deserialize and the derives in src/args/delegate.rs succeed.

In `@src/compact/instruction.rs`:
- Around line 5-37: The code currently truncates the program_id index by casting
index_of(... ) to u8 in Instruction::from_instruction which silently wraps on
overflow; change Instruction::from_instruction to perform a bounds-checked
conversion instead (e.g. use u8::try_from(...) or usize::try_into(...) and
propagate errors) — update the function signature to return Result<Instruction,
Error> (or another appropriate error type), check the program_id conversion
result and return Err on overflow, and adjust callers to handle the Result; keep
the accounts mapping logic as-is but ensure any similar numeric casts are also
bounds-checked where index_of is used.

In `@src/encryption/mod.rs`:
- Around line 92-158: The current XOR keystream must be replaced with AEAD: in
encrypt_for_validator_with_ephemeral derive a 32-byte AEAD key from the DH
shared secret using HKDF-SHA256 (use a domain-specific info string like
"encrypted-actions-v1"), generate a random 24-byte XChaCha20-Poly1305 nonce,
encrypt the plaintext with XChaCha20-Poly1305 and store ephemeral_pubkey + nonce
+ ciphertext in EncryptedActionsV1; in decrypt_for_validator deserialize
ephemeral_pubkey and nonce, derive the same AEAD key with HKDF-SHA256 from the
DH shared secret and call XChaCha20-Poly1305::decrypt to both authenticate and
decrypt (remove xor_with_stream usage), and update EncryptedActionsV1 to include
the 24-byte nonce; add hkdf and chacha20poly1305 crates (and a secure RNG for
nonce generation) and handle/propagate AEAD errors instead of silent XOR output.

In `@src/instruction_builder/delegate_with_actions.rs`:
- Around line 26-29: The docstring above the DecryptFn type alias contains a
typo ("Dencrypt"); update the comment to read "Decrypt serialized-encrypted
bytes into serialized-decrypted bytes." so the description accurately matches
the DecryptFn symbol and behavior.
- Around line 274-282: The inline reorder comment is wrong: update the comment
above the assertions to reflect the actual asserted order (a, c, e, b, d) and
corresponding indices (0, 1, 2, 3, 4) so it matches the assertions referencing
actions.signer_count and actions.pubkeys[0..4]; ensure the comment labels which
entries are signers (a,c,e) and non-signers (b,d) to match the assert_eq!
checks.
- Around line 122-137: The cleartext branch currently clones
compact_instructions (compact_instructions.clone()) causing an unnecessary
allocation; instead pattern-match on private (e.g., use match private {
Some(encrypt) => ..., None => ... }) so you can move compact_instructions into
Instructions::ClearText { instructions: compact_instructions } without cloning.
Ensure the encrypted branch still borrows/serializes compact_instructions as
needed before moving, and remove the .clone() call in the
Instructions::ClearText arm.

In `@src/processor/fast/delegate_with_actions.rs`:
- Around line 218-256: The code re-serializes args.actions into action_data
before storing it in delegation_record_account which wastes CPU; instead capture
and store the original raw byte slice for the actions from the input buffer (the
same bytes used to deserialize args.actions) and write that slice into
action_bytes. Update the create_pda size calculation and the length check to use
the original slice length, and remove the bincode::serialize call; locate the
earlier deserialization of args/actions (where the input buffer is parsed) and
compute or retain the byte offset/borrowed slice to pass here, ensuring
DelegationRecord::size_with_discriminator(), create_pda,
delegation_record_account, action_bytes and the subsequent copy_from_slice use
that original slice.
- Around line 146-216: Extract the duplicated PDA seed validation into a shared
helper like validate_pda_seeds(seeds: &[Vec<u8>], delegated_account: &Account,
program_id: &Address) and replace the big match in delegate_with_actions.rs
(which currently inspects args.delegate.seeds, builds seeds_to_validate, calls
Address::find_program_address and returns DlpError::TooManySeeds) and the
analogous block in delegate.rs with calls to that helper; ensure the helper
handles the zero-seeds case explicitly (return a clear error instead of falling
through to the TooManySeeds arm) and preserves the existing address_eq checks
and ProgramError::InvalidSeeds logging behavior used around
Address::find_program_address.
- Around line 285-290: The copy_from_slice can panic when
delegate_buffer_account and delegated_account differ in length; before copying
(around the block using delegate_buffer_account, delegated_account,
try_borrow_mut/try_borrow and copy_from_slice) either resize the
delegated_account to match delegate_buffer_account's data length (as done in
finalize.rs via resize/realloc) or add an explicit length check and return an
error if sizes differ—ensure you reference the buffer length and adjust
delegated_account size before calling copy_from_slice.

In `@src/state/utils/try_from_bytes.rs`:
- Around line 8-16: The length check currently only rejects short buffers and
allows trailing bytes; change the validation to require exact length by
replacing the current conditional (if data.len() < expected_len) with an
exact-match check (if data.len() != expected_len) so that
bytemuck::try_from_bytes::<Self>(&data[8..expected_len]) always gets a slice of
the exact expected size; keep the existing error variant
($crate::error::DlpError::InvalidDataLength) for mismatched lengths and leave
the discriminator check and bytemuck call (Self::discriminator().to_bytes(),
bytemuck::try_from_bytes::<Self>) unchanged.

In `@tests/test_delegate_with_actions.rs`:
- Around line 30-77: The test
test_delegate_with_actions_bincode_roundtrip_compact_payload only checks counts;
extend it to assert at least one decoded compact instruction's fields to catch
encoding regressions: after matching Instructions::ClearText { instructions },
decode the first instruction and assert its program_id index (or the resolved
Pubkey via args.actions.pubkeys), the account meta indices and signer/writable
flags, and the exact data bytes match the original vec![1,2,3] (and similarly
for the second instruction's data vec![9,9]); use the existing
DelegateWithActionsArgs / Instructions::ClearText structures and ix.data slice
to locate the decoded payload and compact::MAX_PUBKEYS for pubkey resolution.
- Around line 11-13: The mock function test_encrypt currently returns the input
unchanged (a no-op), which can be confusing; update the test by adding a brief
comment above the test_encrypt function explaining that this is an intentional
no-op used to validate builder wiring/encrypted code path in tests (i.e., it
simulates encryption without transforming bytes), and optionally note where to
replace it with a real encryptor if needed (reference function name:
test_encrypt).

---

Outside diff comments:
In @.github/workflows/run-tests.yml:
- Around line 47-56: The lint job currently runs "cargo +nightly fmt -- --check"
but does not install the nightly toolchain on the runner; update the "lint" job
to install and activate the nightly toolchain before running cargo commands (for
example run a step that installs rustup/toolchain and executes "rustup toolchain
install nightly" and/or "rustup default nightly" or use the actions-rs/toolchain
action) so that "cargo +nightly fmt -- --check" and "cargo clippy --
--deny=warnings" run successfully.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95ca88d and cbf6d6e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .github/workflows/run-tests.yml
  • Cargo.toml
  • src/args/delegate.rs
  • src/args/delegate_with_actions.rs
  • src/args/mod.rs
  • src/compact/account_meta.rs
  • src/compact/instruction.rs
  • src/compact/mod.rs
  • src/discriminator.rs
  • src/encryption/mod.rs
  • src/instruction_builder/delegate_with_actions.rs
  • src/instruction_builder/mod.rs
  • src/lib.rs
  • src/processor/fast/delegate_with_actions.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/utils/requires.rs
  • src/state/utils/try_from_bytes.rs
  • tests/test_delegate_with_actions.rs

@snawaz snawaz force-pushed the snawaz/delegate-with-actions branch from cbf6d6e to 197ae71 Compare February 24, 2026 10:51
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (10)
Cargo.toml (1)

70-70: Serde exact pin =1.0.226 is still overly restrictive.

This was flagged in a previous review. The = pin prevents consumers from resolving any compatible patch update, while transitive deps only require ^1.0.226.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 70, The serde dependency is pinned exactly which blocks
consumers from resolving patch updates; update the serde entry (the serde = {
version = "=1.0.226", features = ["derive"] } line) to a non-exact spec such as
version = "1.0.226" (or ">=1.0.226, <2.0.0") so Cargo will allow compatible
patch releases while keeping the derive feature enabled.
src/compact/instruction.rs (1)

21-21: program_id index truncation via as u8 is still unguarded.

The as u8 cast silently wraps if index_of returns a value > 255. While the account meta path uses try_new with a 6-bit bounds check (and panics on overflow), the program ID path has no equivalent guard.

💡 Suggested fix
-            program_id: index_of(ix.program_id, false) as u8,
+            program_id: u8::try_from(index_of(ix.program_id, false))
+                .expect("program_id index must fit in u8"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/instruction.rs` at line 21, The program_id field is unsafely
truncated with `as u8` after calling `index_of(ix.program_id, false)`; update
the code to perform a checked conversion instead of silent wrapping — e.g.
capture the usize result of `index_of(ix.program_id, false)`, validate it is <=
255 (or use `u8::try_from(...)`), and return or propagate an error (or panic
consistently like the account meta path) when it overflows; change the
assignment to use the checked value for `program_id` rather than `as u8` so
overflows are detected and handled.
src/state/utils/try_from_bytes.rs (1)

8-17: Accepting trailing bytes is reasonable for Solana account data, but verify it's intentional.

The switch from data.len() < 8 + bytemuck::try_from_bytes(&data[8..]) (which implicitly rejected oversized buffers via bytemuck) to data.len() < expected_len + &data[8..expected_len] now explicitly tolerates trailing bytes. This is typically correct for Solana accounts (which may be reallocated larger), but it is a semantic change worth confirming as intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/state/utils/try_from_bytes.rs` around lines 8 - 17, The change in
try_from_bytes (use of expected_len and slicing to &data[8..expected_len]) now
intentionally permits trailing bytes; decide and make it explicit: if allowing
trailing bytes is intended for Solana reallocated accounts, add a clarifying
comment above the check referencing expected_len and why trailing bytes are
accepted and keep the current logic in
try_from_bytes::<Self>(&data[8..expected_len]); otherwise restore strict
validation by replacing the length check with data.len() == expected_len (or
data.len() != expected_len to return InvalidDataLength) and slice the payload
accordingly (or switch back to bytemuck::try_from_bytes(&data[8..]) to preserve
previous rejection of oversized buffers), updating the code paths around
expected_len, Self::discriminator(), and the InvalidDelegationRecordData error
to match.
src/processor/fast/delegate_with_actions.rs (2)

146-205: ⚠️ Potential issue | 🟡 Minor

Handle empty seed list explicitly.

seeds.len() == 0 currently falls into the _ => TooManySeeds arm, which is misleading for this case.

🩹 Suggested fix
-        let seeds_to_validate: &[&[u8]] = match args.delegate.seeds.len() {
+        let seeds_to_validate: &[&[u8]] = match args.delegate.seeds.len() {
+            0 => return Err(ProgramError::InvalidSeeds),
             1 => &[&args.delegate.seeds[0]],
             2 => &[&args.delegate.seeds[0], &args.delegate.seeds[1]],
             3 => &[
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 146 - 205, The
match on args.delegate.seeds.len() treats 0 as an unexpected "too many" seeds;
update the match in delegate_with_actions.rs (the block that builds
seeds_to_validate guarded by is_on_curve_fast(delegated_account.address()) and
owner_program.address()) to include an explicit 0 => return Err(...) arm that
returns a clear error (e.g., DlpError::EmptySeeds or DlpError::NoSeeds) instead
of falling through to the default TooManySeeds; if that error variant doesn't
exist, add it to the DlpError enum and use .into() like the others so callers of
is_on_curve_fast/delegate logic get an accurate message.

285-289: ⚠️ Potential issue | 🟠 Major

Avoid potential panic on buffer copy length mismatch.

copy_from_slice will panic if the delegated account and buffer lengths differ; add a length check or resize before copying.

🛡️ Suggested guard
     if !delegate_buffer_account.is_data_empty() {
         let mut delegated_data = delegated_account.try_borrow_mut()?;
         let delegate_buffer_data = delegate_buffer_account.try_borrow()?;
+        if delegated_data.len() != delegate_buffer_data.len() {
+            return Err(DlpError::InvalidDataLength.into());
+        }
         (*delegated_data).copy_from_slice(&delegate_buffer_data);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 285 - 289, The
copy_from_slice call can panic if the source and destination lengths differ;
before calling copy_from_slice in the block that uses delegate_buffer_account
and delegated_account (after try_borrow_mut()/try_borrow()), check that
delegated_data.len() == delegate_buffer_data.len() and if not return an
appropriate error (or handle resizing if supported) instead of copying; update
the code path around copy_from_slice to validate lengths and bail out with a
clear ProgramError or custom error to avoid panics.
src/args/delegate.rs (1)

2-7: Verify solana-program serde feature for Pubkey serialization.

DelegateArgs now derives Serialize/Deserialize, which requires solana-program to enable its serde feature; otherwise this will not compile.

#!/bin/bash
# Verify solana-program dependency features include serde.
rg -n 'solana-program' -g 'Cargo.toml'
rg -n 'solana-program\s*=\s*\{[^}]*features\s*=\s*\[[^]]*serde' -g 'Cargo.toml'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/args/delegate.rs` around lines 2 - 7, DelegateArgs now derives
Serialize/Deserialize and includes solana_program::pubkey::Pubkey, which
requires the solana-program crate to be compiled with its "serde" feature;
update the Cargo.toml dependency for solana-program to enable the serde feature
(or remove Serialize/Deserialize from DelegateArgs) so the Pubkey can be
serialized. Specifically, either add the "features = [\"serde\"]" flag to the
solana-program dependency entry or remove/replace the Serialize/Deserialize
derives on the DelegateArgs struct to avoid requiring solana-program serde
support.
src/instruction_builder/delegate_with_actions.rs (3)

21-29: ⚠️ Potential issue | 🟡 Minor

Fix typo in DecryptFn docstring.

"Dencrypt" should be "Decrypt".

✏️ Suggested fix
-/// Dencrypt serialized-encrypted bytes into serialized-decrypted bytes.
+/// Decrypt serialized-encrypted bytes into serialized-decrypted bytes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 21 - 29, The
docstring for the DecryptFn type has a typo ("Dencrypt")—update the comment
immediately above pub type DecryptFn to read "Decrypt serialized-encrypted bytes
into serialized-decrypted bytes." so the description matches the symbol
DecryptFn and mirrors the EncryptFn docstring style.

252-297: ⚠️ Potential issue | 🟡 Minor

Fix reorder/mapping comments to match assertions.

The comment says a, c, e, d, b but assertions expect a, c, e, b, d, and the old→new mapping is off.

✏️ Suggested fix
-        // reordered: a, c, e, d, b
-        //            0, 1, 2, 3, 4
+        // reordered: a, c, e, b, d
+        //            0, 1, 2, 3, 4
@@
-        // old->new mapping: a(0)->0, b(1)->4, c(2)->1, d(3)->3, e(4)->2
+        // old->new mapping: d(0)->4, a(1)->0, c(2)->1, b(3)->3, e(4)->2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 252 - 297, The
comment above test_compact_post_delegation_actions is incorrect: update the
inline reorder/mapping comments to match the actual assertions (pubkey order a,
c, e, b, d and old->new mapping a(0)->0, b(1)->4, c(2)->1, d(3)->4? actually
d->4? wait) — specifically change the line that currently reads "reordered: a,
c, e, d, b" to "reordered: a, c, e, b, d" and fix the indexed comment below to
show the new indices "0, 1, 2, 3, 4"; also correct the "old->new mapping"
comment to reflect the asserted mapping a(0)->0, b(1)->4, c(2)->1, d(3)->3,
e(4)->2 so the comment matches the assertions in
test_compact_post_delegation_actions (update any mismatched numeric mappings
accordingly).

91-137: 🧹 Nitpick | 🔵 Trivial

Avoid cloning compact_instructions in the cleartext path.

This can move the vector instead of allocating a clone.

♻️ Suggested refactor
-    let compact_payload = if let Some(encrypt) = private {
-        Instructions::Encrypted {
-            // first serialize the compact-instructions, then encrypt it,
-            // note that the final serialized-encrypted bytes will be
-            // serialized again later by the caller.
-            instructions: encrypt(
-                &bincode::serialize(&compact_instructions).expect(
-                    "compact instruction serialization should not fail",
-                ),
-            ),
-        }
-    } else {
-        Instructions::ClearText {
-            instructions: compact_instructions.clone(),
-        }
-    };
+    let compact_payload = match private {
+        Some(encrypt) => Instructions::Encrypted {
+            // first serialize the compact-instructions, then encrypt it,
+            // note that the final serialized-encrypted bytes will be
+            // serialized again later by the caller.
+            instructions: encrypt(
+                &bincode::serialize(&compact_instructions).expect(
+                    "compact instruction serialization should not fail",
+                ),
+            ),
+        },
+        None => Instructions::ClearText {
+            instructions: compact_instructions,
+        },
+    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 91 - 137, The
cleartext branch needlessly clones compact_instructions; in
compact_post_delegation_actions return the compact_instructions by move instead
of cloning. Replace the Instructions::ClearText arm to take ownership of the
compact_instructions variable (no .clone()), keeping the encrypted branch
unchanged (it can serialize by reference); update references to
compact_instructions accordingly so the value is moved into
Instructions::ClearText rather than cloned.
src/encryption/mod.rs (1)

92-158: ⚠️ Potential issue | 🟠 Major

Replace XOR stream with authenticated encryption (AEAD).

The XOR keystream provides confidentiality only; tampering or wrong-key decrypts are undetectable. For delegated actions, add integrity by deriving an AEAD key from the DH shared secret (e.g., HKDF-SHA256 with domain-separated info) and encrypt with XChaCha20-Poly1305, storing a nonce alongside the ephemeral pubkey. Decrypt should authenticate before returning plaintext.

🧩 Minimal structural change (nonce field)
 pub struct EncryptedActionsV1 {
     /// Ephemeral X25519 public key created by the sender for this payload.
     pub ephemeral_pubkey: [u8; KEY_LEN],
+    /// AEAD nonce (XChaCha20-Poly1305 uses 24 bytes).
+    pub nonce: [u8; 24],
 
     /// Encrypted compact-action bytes.
     pub ciphertext: Vec<u8>,
 }
Rust X25519 HKDF-SHA256 XChaCha20-Poly1305 authenticated encryption pattern
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/encryption/mod.rs` around lines 92 - 158, The current implementation
(encrypt_for_validator_with_ephemeral / decrypt_for_validator / xor_with_stream
/ EncryptedActionsV1) uses an unauthenticated XOR stream; replace it with AEAD:
derive a symmetric key from the X25519 shared secret using HKDF-SHA256 with
domain-separated info, generate a random XChaCha20-Poly1305 nonce per
encryption, and store that nonce in EncryptedActionsV1 alongside
ephemeral_pubkey and ciphertext; in encrypt_for_validator_with_ephemeral use
XChaCha20-Poly1305 to seal (producing ciphertext+tag), and in
decrypt_for_validator use the same HKDF derivation and XChaCha20-Poly1305 to
open and return plaintext, failing if authentication fails; update
EncryptedActionsV1 to include nonce, remove/replace xor_with_stream, and change
decrypt_for_validator’s signature to return an appropriate error type so AEAD
open failures can be propagated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/run-tests.yml:
- Line 54: The workflow currently installs and uses an unpinned nightly via the
run step that calls "rustup component add --toolchain
nightly-x86_64-unknown-linux-gnu rustfmt" and "cargo +nightly fmt -- --check";
change both invocations to use a specific pinned nightly (e.g.,
nightly-2025-12-01) so the toolchain and cargo+toolchain invocations are
deterministic: update the rustup --toolchain value to the pinned nightly
identifier and change the cargo +nightly invocation to cargo +nightly-YYYY-MM-DD
fmt -- --check (matching the same pinned date).

In `@Cargo.toml`:
- Line 74: The dependency entry for solana-sdk uses an unbounded version
("solana-sdk = { version = \">=1.16\", optional = true }") which risks pulling
in breaking major releases; change the version requirement to include an upper
bound consistent with solana-program (e.g., use a caret or range that caps at
the next major version) so it will allow 1.16.x-compatible updates but prevent
accidental 2.x upgrades—update the solana-sdk dependency declaration in
Cargo.toml to mirror the solana-program constraint.

In `@src/compact/account_meta.rs`:
- Around line 15-16: The AccountMeta public tuple field allows constructing
invalid values and Deserialize bypasses try_new validation; make the inner field
private by changing AccountMeta(pub u8) to AccountMeta(u8) and expose a
validated constructor (try_new) and accessor (e.g., .get()); implement a custom
serde::Deserialize for AccountMeta (or use serde's deserialize_with) that calls
the same validation logic (the try_new check for index <= 63) so deserialized
values are validated, and update any call sites that construct AccountMeta
directly to use the validated constructor or accessor.

In `@src/compact/instruction.rs`:
- Around line 5-38: The program_id field in Instruction is stored as u8 but not
validated to the same 6-bit cap used for AccountMeta indices; update the
from_instruction constructor (the Instruction::from_instruction function, and
the program_id field on struct Instruction) to obtain the program index via
index_of(ix.program_id, false), validate it fits in 6 bits (<= 63) and then
store it (as u8) — or alternatively change the struct type and add a clear
comment if program IDs come from a separate table; ensure the validation uses
the same error/expect pattern used for compact::AccountMeta so failures are
consistent and document the choice in code comments.

---

Duplicate comments:
In `@Cargo.toml`:
- Line 70: The serde dependency is pinned exactly which blocks consumers from
resolving patch updates; update the serde entry (the serde = { version =
"=1.0.226", features = ["derive"] } line) to a non-exact spec such as version =
"1.0.226" (or ">=1.0.226, <2.0.0") so Cargo will allow compatible patch releases
while keeping the derive feature enabled.

In `@src/args/delegate.rs`:
- Around line 2-7: DelegateArgs now derives Serialize/Deserialize and includes
solana_program::pubkey::Pubkey, which requires the solana-program crate to be
compiled with its "serde" feature; update the Cargo.toml dependency for
solana-program to enable the serde feature (or remove Serialize/Deserialize from
DelegateArgs) so the Pubkey can be serialized. Specifically, either add the
"features = [\"serde\"]" flag to the solana-program dependency entry or
remove/replace the Serialize/Deserialize derives on the DelegateArgs struct to
avoid requiring solana-program serde support.

In `@src/compact/instruction.rs`:
- Line 21: The program_id field is unsafely truncated with `as u8` after calling
`index_of(ix.program_id, false)`; update the code to perform a checked
conversion instead of silent wrapping — e.g. capture the usize result of
`index_of(ix.program_id, false)`, validate it is <= 255 (or use
`u8::try_from(...)`), and return or propagate an error (or panic consistently
like the account meta path) when it overflows; change the assignment to use the
checked value for `program_id` rather than `as u8` so overflows are detected and
handled.

In `@src/encryption/mod.rs`:
- Around line 92-158: The current implementation
(encrypt_for_validator_with_ephemeral / decrypt_for_validator / xor_with_stream
/ EncryptedActionsV1) uses an unauthenticated XOR stream; replace it with AEAD:
derive a symmetric key from the X25519 shared secret using HKDF-SHA256 with
domain-separated info, generate a random XChaCha20-Poly1305 nonce per
encryption, and store that nonce in EncryptedActionsV1 alongside
ephemeral_pubkey and ciphertext; in encrypt_for_validator_with_ephemeral use
XChaCha20-Poly1305 to seal (producing ciphertext+tag), and in
decrypt_for_validator use the same HKDF derivation and XChaCha20-Poly1305 to
open and return plaintext, failing if authentication fails; update
EncryptedActionsV1 to include nonce, remove/replace xor_with_stream, and change
decrypt_for_validator’s signature to return an appropriate error type so AEAD
open failures can be propagated.

In `@src/instruction_builder/delegate_with_actions.rs`:
- Around line 21-29: The docstring for the DecryptFn type has a typo
("Dencrypt")—update the comment immediately above pub type DecryptFn to read
"Decrypt serialized-encrypted bytes into serialized-decrypted bytes." so the
description matches the symbol DecryptFn and mirrors the EncryptFn docstring
style.
- Around line 252-297: The comment above test_compact_post_delegation_actions is
incorrect: update the inline reorder/mapping comments to match the actual
assertions (pubkey order a, c, e, b, d and old->new mapping a(0)->0, b(1)->4,
c(2)->1, d(3)->4? actually d->4? wait) — specifically change the line that
currently reads "reordered: a, c, e, d, b" to "reordered: a, c, e, b, d" and fix
the indexed comment below to show the new indices "0, 1, 2, 3, 4"; also correct
the "old->new mapping" comment to reflect the asserted mapping a(0)->0, b(1)->4,
c(2)->1, d(3)->3, e(4)->2 so the comment matches the assertions in
test_compact_post_delegation_actions (update any mismatched numeric mappings
accordingly).
- Around line 91-137: The cleartext branch needlessly clones
compact_instructions; in compact_post_delegation_actions return the
compact_instructions by move instead of cloning. Replace the
Instructions::ClearText arm to take ownership of the compact_instructions
variable (no .clone()), keeping the encrypted branch unchanged (it can serialize
by reference); update references to compact_instructions accordingly so the
value is moved into Instructions::ClearText rather than cloned.

In `@src/processor/fast/delegate_with_actions.rs`:
- Around line 146-205: The match on args.delegate.seeds.len() treats 0 as an
unexpected "too many" seeds; update the match in delegate_with_actions.rs (the
block that builds seeds_to_validate guarded by
is_on_curve_fast(delegated_account.address()) and owner_program.address()) to
include an explicit 0 => return Err(...) arm that returns a clear error (e.g.,
DlpError::EmptySeeds or DlpError::NoSeeds) instead of falling through to the
default TooManySeeds; if that error variant doesn't exist, add it to the
DlpError enum and use .into() like the others so callers of
is_on_curve_fast/delegate logic get an accurate message.
- Around line 285-289: The copy_from_slice call can panic if the source and
destination lengths differ; before calling copy_from_slice in the block that
uses delegate_buffer_account and delegated_account (after
try_borrow_mut()/try_borrow()), check that delegated_data.len() ==
delegate_buffer_data.len() and if not return an appropriate error (or handle
resizing if supported) instead of copying; update the code path around
copy_from_slice to validate lengths and bail out with a clear ProgramError or
custom error to avoid panics.

In `@src/state/utils/try_from_bytes.rs`:
- Around line 8-17: The change in try_from_bytes (use of expected_len and
slicing to &data[8..expected_len]) now intentionally permits trailing bytes;
decide and make it explicit: if allowing trailing bytes is intended for Solana
reallocated accounts, add a clarifying comment above the check referencing
expected_len and why trailing bytes are accepted and keep the current logic in
try_from_bytes::<Self>(&data[8..expected_len]); otherwise restore strict
validation by replacing the length check with data.len() == expected_len (or
data.len() != expected_len to return InvalidDataLength) and slice the payload
accordingly (or switch back to bytemuck::try_from_bytes(&data[8..]) to preserve
previous rejection of oversized buffers), updating the code paths around
expected_len, Self::discriminator(), and the InvalidDelegationRecordData error
to match.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbf6d6e and 197ae71.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .github/workflows/run-tests.yml
  • Cargo.toml
  • src/args/delegate.rs
  • src/args/delegate_with_actions.rs
  • src/args/mod.rs
  • src/compact/account_meta.rs
  • src/compact/instruction.rs
  • src/compact/mod.rs
  • src/discriminator.rs
  • src/encryption/mod.rs
  • src/instruction_builder/delegate_with_actions.rs
  • src/instruction_builder/mod.rs
  • src/lib.rs
  • src/processor/fast/delegate_with_actions.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/utils/requires.rs
  • src/state/utils/try_from_bytes.rs
  • tests/test_delegate_with_actions.rs

@snawaz snawaz force-pushed the snawaz/delegate-with-actions branch from 197ae71 to fe2d14a Compare February 24, 2026 11:35
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (13)
.github/workflows/run-tests.yml (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Pin the nightly toolchain to a specific date for reproducible fmt.

An unpinned nightly can change formatting day‑to‑day and cause CI churn. Pin the nightly date in both the rustup component add and the cargo +nightly invocation.

🔧 Suggested change
-        run: rustup component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt && cargo +nightly fmt -- --check
+        run: rustup component add --toolchain nightly-2025-12-01-x86_64-unknown-linux-gnu rustfmt && cargo +nightly-2025-12-01 fmt -- --check
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/run-tests.yml at line 54, The workflow uses an unpinned
nightly toolchain; replace the generic "nightly-x86_64-unknown-linux-gnu" and
the "cargo +nightly" invocation with a date-pinned toolchain (e.g.
"nightly-YYYY-MM-DD-x86_64-unknown-linux-gnu") and use the same pinned label in
cargo (e.g. "cargo +nightly-YYYY-MM-DD fmt -- --check") so both rustup component
add and cargo use the identical pinned nightly for reproducible formatting.
Cargo.toml (2)

70-70: Exact serde pin remains unnecessarily restrictive.

The =1.0.226 pin was flagged in a prior review. The transitive requirements (solana-address ^1.0.226, solana-sdk ^1.0.217) are compatible without an exact pin. Consider relaxing to "^1.0.226" or updating.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 70, The serde dependency in Cargo.toml is pinned exactly
("=1.0.226"), which is unnecessarily restrictive; update the serde entry (the
line containing serde = { version = "=1.0.226", features = ["derive"] }) to use
a compatible caret requirement (e.g., version = "^1.0.226") or a newer
compatible version while preserving features = ["derive"] so transitive deps
like solana-address/solana-sdk can resolve correctly.

74-74: Unbounded solana-sdk version range still present.

This was flagged in a prior review. >=1.16 has no upper bound, risking breakage across major versions. Consider ">=1.16, <3.0.0" to match the solana-program constraint on Line 52.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 74, The solana-sdk dependency in Cargo.toml uses an
unbounded version range ("solana-sdk = { version = \">=1.16\", optional = true
}"), which risks breaking changes; update the solana-sdk version constraint to
include an upper bound consistent with solana-program (for example ">=1.16,
<3.0.0") so it matches the solana-program constraint and prevents automatic
major-version upgrades; modify the solana-sdk dependency entry accordingly and
keep the optional = true flag.
src/args/delegate.rs (1)

2-7: Pubkey serde support requires feature flag on solana-program — previously flagged.

Deriving Serialize/Deserialize on DelegateArgs requires Pubkey to implement those traits. Depending on the resolved solana-program version, this may require features = ["serde"] on the solana-program dependency in Cargo.toml. This was raised in a prior review.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/args/delegate.rs` around lines 2 - 7, The DelegateArgs derives include
Serialize/Deserialize but use solana_program::pubkey::Pubkey which only provides
serde impl when solana-program is compiled with the "serde" feature; update the
Cargo.toml solana-program dependency to enable features = ["serde"] (or
alternatively remove Serialize/Deserialize from DelegateArgs or implement custom
serde for Pubkey) so the Pubkey type used by DelegateArgs can satisfy the
Serialize/Deserialize derives.
src/instruction_builder/delegate_with_actions.rs (3)

26-29: Typo: "Dencrypt" → "Decrypt" in DecryptFn docstring.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 26 - 29, The
docstring for the type alias DecryptFn contains a typo ("Dencrypt"); update the
comment above the pub type DecryptFn to read "Decrypt serialized-encrypted bytes
into serialized-decrypted bytes." so the wording correctly references DecryptFn
and matches the function's purpose.

91-144: Unnecessary .clone() of compact_instructions in the cleartext path.

Line 135 clones the entire vector when the encrypted branch doesn't need it afterward. Using match and moving the vector in the None arm avoids the allocation.

♻️ Proposed fix
-    let compact_payload = if let Some(encrypt) = private {
-        Instructions::Encrypted {
+    let compact_payload = match private {
+        Some(encrypt) => Instructions::Encrypted {
             instructions: encrypt(
                 &bincode::serialize(&compact_instructions).expect(
                     "compact instruction serialization should not fail",
                 ),
             ),
-        }
-    } else {
-        Instructions::ClearText {
-            instructions: compact_instructions.clone(),
-        }
+        },
+        None => Instructions::ClearText {
+            instructions: compact_instructions,
+        },
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 91 - 144, In
compact_post_delegation_actions remove the unnecessary
compact_instructions.clone() by moving compact_instructions into the cleartext
arm instead of cloning it: change the if let Some(encrypt) = private { ... }
else { Instructions::ClearText { instructions: compact_instructions.clone(), } }
to a match or if/else that consumes compact_instructions (e.g. match private {
Some(encrypt) => Instructions::Encrypted { instructions:
encrypt(&bincode::serialize(&compact_instructions).expect("...")) }, None =>
Instructions::ClearText { instructions: compact_instructions }, }), ensuring you
still serialize before encrypting and keep references to the same
compact_instructions identifier and the Instructions enum variants.

274-276: Comment does not match the actual reordered order.

The comment says a, c, e, d, b but the assertions (lines 278–282) and the partitioning logic produce a, c, e, b, d. This is because d is inserted first (as program_id) and gets a lower original index than b, so after partitioning non-signers, b (original index 3) comes before d (original index 0) due to partition swap order.

✏️ Fix
-        // reordered: a, c, e, d, b
-        //            0, 1, 2, 3, 4
+        // reordered: a, c, e, b, d
+        //            0, 1, 2, 3, 4
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 274 - 276, The
inline comment describing the reordered list is incorrect: it says "a, c, e, d,
b" but actual logic (inserting d as program_id before partitioning non-signers
and the subsequent partition swap) yields "a, c, e, b, d"; update the comment to
reflect "a, c, e, b, d" (and optionally note that d was inserted as program_id
and thus ends up after b due to original indices) so it matches the assertions
in the surrounding code and the partitioning behavior.
src/processor/fast/delegate_with_actions.rs (2)

285-290: Missing length guard before copy_from_slice — potential runtime panic.

copy_from_slice will panic if delegate_buffer_account and delegated_account have different data lengths. The code only checks is_data_empty() but doesn't verify or adjust sizes. Compare to finalize.rs, which calls resize() before the copy. Either resize the delegated account or add an explicit length check with a proper error return.

🛡️ Proposed fix (explicit length check)
     if !delegate_buffer_account.is_data_empty() {
+        let delegate_buffer_data = delegate_buffer_account.try_borrow()?;
+        if delegated_account.data_len() != delegate_buffer_data.len() {
+            return Err(DlpError::InvalidDataLength.into());
+        }
         let mut delegated_data = delegated_account.try_borrow_mut()?;
-        let delegate_buffer_data = delegate_buffer_account.try_borrow()?;
         (*delegated_data).copy_from_slice(&delegate_buffer_data);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 285 - 290, The
copy_from_slice call in delegate_with_actions.rs can panic because you only
check delegate_buffer_account.is_data_empty() but not that delegated_account and
delegate_buffer_account have equal data lengths; update the code in the block
handling delegate_buffer_account and delegated_account to either resize
delegated_account like finalize.rs does (call the same resize helper before
copying) or perform an explicit length check comparing
delegated_account.try_borrow_mut()?.len() (or the account data lengths) against
delegate_buffer_account.try_borrow()?.len() and return a proper error instead of
calling copy_from_slice when sizes differ; specifically modify the logic around
delegate_buffer_account, delegated_account, copy_from_slice and is_data_empty to
ensure lengths match or delegated_account is resized prior to copying.

146-216: Extract shared PDA seed validation to eliminate duplication with delegate.rs.

This ~70-line match block is duplicated from the existing delegate processor (differing only in the field path: args.delegate.seeds vs args.seeds). Additionally, seeds.len() == 0 silently falls through to the _ => arm returning TooManySeeds, which is misleading. A shared helper like validate_pda_seeds(seeds, delegated_account, program_id) with an explicit zero-seeds check would improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 146 - 216, Extract
the duplicated PDA seed validation into a shared helper like
validate_pda_seeds(seeds: &[Vec<u8>], delegated_account: &Address,
owner_program: &ProgramId) that performs the is_on_curve_fast check, computes
program_id (handling pinocchio_system::ID -> crate::fast::ID), builds the seed
slice for Address::find_program_address, and compares the derived PDA to
delegated_account.address(); replace the large match in delegate_with_actions.rs
(which reads args.delegate.seeds) and the duplicate block in delegate.rs (which
reads args.seeds) with calls to this helper; additionally, change the logic so
zero-length seeds return a clear error (not DlpError::TooManySeeds) and keep
existing error returns ProgramError::InvalidSeeds and DlpError::TooManySeeds
symbols for other invalid cases.
tests/test_delegate_with_actions.rs (1)

11-13: Add a brief doc comment explaining the identity mock.

The test_encrypt function is a no-op that returns input unchanged. A one-line comment would clarify this is intentional for testing the encrypted code path without real cryptography.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_delegate_with_actions.rs` around lines 11 - 13, Add a one-line doc
comment above the test_encrypt function explaining that it is an identity/no-op
mock used to exercise the encrypted code path in tests without performing real
cryptography; locate the test_encrypt(serialized: &[u8]) -> Vec<u8> function and
add the brief doc comment (e.g. "/// No-op identity encrypt used in tests") to
make the intent explicit.
src/compact/instruction.rs (1)

19-21: program_id index is not bounds-checked, unlike account meta indices.

Line 21 casts index_of(ix.program_id, false) to u8 without validation, while account meta indices on line 27-32 are validated via try_new (which rejects index >= MAX_PUBKEYS). Since both reference the same pubkey table, program_id should have the same bounds check.

🔧 Proposed fix
-            program_id: index_of(ix.program_id, false) as u8,
+            program_id: {
+                let idx = index_of(ix.program_id, false);
+                assert!(idx < compact::MAX_PUBKEYS, "program_id index must fit in 6 bits");
+                idx as u8
+            },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/instruction.rs` around lines 19 - 21, The program_id index is
cast to u8 without bounds-checking while account meta indices use try_new and
enforce MAX_PUBKEYS; update the Instruction construction so the program_id index
is validated the same way: call the same try_new validation used for account
meta indices on index_of(ix.program_id, false) and propagate or handle the error
consistently before assigning to the Instruction.program_id field (same pattern
as used for account metas and MAX_PUBKEYS).
src/compact/account_meta.rs (1)

15-16: Public inner field bypasses validation on deserialization.

AccountMeta(pub u8) allows constructing instances with arbitrary bytes, and the derived Deserialize doesn't validate the index range. While the processor does validate indices post-deserialization, making the field private would provide defense-in-depth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/account_meta.rs` around lines 15 - 16, Change the public tuple
field to a private field (replace "pub struct AccountMeta(pub u8);" with "pub
struct AccountMeta(u8);"), add a validated constructor (e.g., pub fn
try_from(u8) -> Result<AccountMeta, _> or impl TryFrom<u8> for AccountMeta) and
a public accessor (pub fn index(&self) -> u8), and ensure deserialization goes
through that validator by implementing Deserialize manually or using serde to
deserialize via the TryFrom/new constructor so out-of-range bytes cannot produce
a valid AccountMeta; reference symbols: AccountMeta, try_from/TryFrom, index,
and Deserialize.
src/encryption/mod.rs (1)

17-24: ⚠️ Potential issue | 🟠 Major

Replace keystream XOR with authenticated encryption (AEAD).
The current XOR stream provides confidentiality only; ciphertexts are malleable and tampering/wrong keys won’t be detected. Use DH → HKDF-SHA256 → XChaCha20-Poly1305 with a random nonce, store nonce alongside the ephemeral pubkey, and return authentication errors on decrypt.

X25519 Diffie-Hellman HKDF XChaCha20-Poly1305 Rust best practice authenticated encryption

Also applies to: 92-158

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/encryption/mod.rs` around lines 17 - 24, The EncryptedActionsV1 struct
and current XOR keystream must be replaced with AEAD: add a random 24-byte nonce
field to EncryptedActionsV1 (alongside ephemeral_pubkey and ciphertext), derive
a shared secret with X25519 DH, run HKDF-SHA256 to produce a 32-byte AEAD key,
and encrypt/decrypt with XChaCha20-Poly1305 (e.g.,
chacha20poly1305::XChaCha20Poly1305) using a secure RNG for the nonce; update
the encrypt and decrypt code paths that reference EncryptedActionsV1,
ephemeral_pubkey, ciphertext and KEY_LEN to perform DH->HKDF->AEAD, store the
nonce in the struct, and surface authentication errors (return an error when
decryption fails authentication) instead of silently producing malformed
plaintext.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/encryption/mod.rs`:
- Around line 160-213: Add a tamper-detection test that, after AEAD is
implemented, encrypts a plaintext via encrypt_for_validator_with_ephemeral using
a fixed validator key and ephemeral, flips a single byte in the resulting
ciphertext, then calls decrypt_for_validator and asserts that it returns an Err
(decryption/authentication failure); reference
encrypt_for_validator_with_ephemeral and decrypt_for_validator to locate where
to hook the test in the tests mod and use fixed keys/ephemeral values similar to
existing tests (e.g., test_encrypt_decrypt_roundtrip) for reproducibility.

In `@src/instruction_builder/delegate_with_actions.rs`:
- Around line 146-172: In reorder_signers_first replace the .unwrap() call
inside the new_index closure with an .expect(...) that includes the failing
old_index and context (e.g., "reorder_signers_first: missing pubkey for
old_index {old_index}") so that if the invariant is violated the panic message
is descriptive; update the new_index closure in the reorder_signers_first
function to call expect on the Option returned by position and include the
old_index value and function name in the message.

---

Duplicate comments:
In @.github/workflows/run-tests.yml:
- Line 54: The workflow uses an unpinned nightly toolchain; replace the generic
"nightly-x86_64-unknown-linux-gnu" and the "cargo +nightly" invocation with a
date-pinned toolchain (e.g. "nightly-YYYY-MM-DD-x86_64-unknown-linux-gnu") and
use the same pinned label in cargo (e.g. "cargo +nightly-YYYY-MM-DD fmt --
--check") so both rustup component add and cargo use the identical pinned
nightly for reproducible formatting.

In `@Cargo.toml`:
- Line 70: The serde dependency in Cargo.toml is pinned exactly ("=1.0.226"),
which is unnecessarily restrictive; update the serde entry (the line containing
serde = { version = "=1.0.226", features = ["derive"] }) to use a compatible
caret requirement (e.g., version = "^1.0.226") or a newer compatible version
while preserving features = ["derive"] so transitive deps like
solana-address/solana-sdk can resolve correctly.
- Line 74: The solana-sdk dependency in Cargo.toml uses an unbounded version
range ("solana-sdk = { version = \">=1.16\", optional = true }"), which risks
breaking changes; update the solana-sdk version constraint to include an upper
bound consistent with solana-program (for example ">=1.16, <3.0.0") so it
matches the solana-program constraint and prevents automatic major-version
upgrades; modify the solana-sdk dependency entry accordingly and keep the
optional = true flag.

In `@src/args/delegate.rs`:
- Around line 2-7: The DelegateArgs derives include Serialize/Deserialize but
use solana_program::pubkey::Pubkey which only provides serde impl when
solana-program is compiled with the "serde" feature; update the Cargo.toml
solana-program dependency to enable features = ["serde"] (or alternatively
remove Serialize/Deserialize from DelegateArgs or implement custom serde for
Pubkey) so the Pubkey type used by DelegateArgs can satisfy the
Serialize/Deserialize derives.

In `@src/compact/account_meta.rs`:
- Around line 15-16: Change the public tuple field to a private field (replace
"pub struct AccountMeta(pub u8);" with "pub struct AccountMeta(u8);"), add a
validated constructor (e.g., pub fn try_from(u8) -> Result<AccountMeta, _> or
impl TryFrom<u8> for AccountMeta) and a public accessor (pub fn index(&self) ->
u8), and ensure deserialization goes through that validator by implementing
Deserialize manually or using serde to deserialize via the TryFrom/new
constructor so out-of-range bytes cannot produce a valid AccountMeta; reference
symbols: AccountMeta, try_from/TryFrom, index, and Deserialize.

In `@src/compact/instruction.rs`:
- Around line 19-21: The program_id index is cast to u8 without bounds-checking
while account meta indices use try_new and enforce MAX_PUBKEYS; update the
Instruction construction so the program_id index is validated the same way: call
the same try_new validation used for account meta indices on
index_of(ix.program_id, false) and propagate or handle the error consistently
before assigning to the Instruction.program_id field (same pattern as used for
account metas and MAX_PUBKEYS).

In `@src/encryption/mod.rs`:
- Around line 17-24: The EncryptedActionsV1 struct and current XOR keystream
must be replaced with AEAD: add a random 24-byte nonce field to
EncryptedActionsV1 (alongside ephemeral_pubkey and ciphertext), derive a shared
secret with X25519 DH, run HKDF-SHA256 to produce a 32-byte AEAD key, and
encrypt/decrypt with XChaCha20-Poly1305 (e.g.,
chacha20poly1305::XChaCha20Poly1305) using a secure RNG for the nonce; update
the encrypt and decrypt code paths that reference EncryptedActionsV1,
ephemeral_pubkey, ciphertext and KEY_LEN to perform DH->HKDF->AEAD, store the
nonce in the struct, and surface authentication errors (return an error when
decryption fails authentication) instead of silently producing malformed
plaintext.

In `@src/instruction_builder/delegate_with_actions.rs`:
- Around line 26-29: The docstring for the type alias DecryptFn contains a typo
("Dencrypt"); update the comment above the pub type DecryptFn to read "Decrypt
serialized-encrypted bytes into serialized-decrypted bytes." so the wording
correctly references DecryptFn and matches the function's purpose.
- Around line 91-144: In compact_post_delegation_actions remove the unnecessary
compact_instructions.clone() by moving compact_instructions into the cleartext
arm instead of cloning it: change the if let Some(encrypt) = private { ... }
else { Instructions::ClearText { instructions: compact_instructions.clone(), } }
to a match or if/else that consumes compact_instructions (e.g. match private {
Some(encrypt) => Instructions::Encrypted { instructions:
encrypt(&bincode::serialize(&compact_instructions).expect("...")) }, None =>
Instructions::ClearText { instructions: compact_instructions }, }), ensuring you
still serialize before encrypting and keep references to the same
compact_instructions identifier and the Instructions enum variants.
- Around line 274-276: The inline comment describing the reordered list is
incorrect: it says "a, c, e, d, b" but actual logic (inserting d as program_id
before partitioning non-signers and the subsequent partition swap) yields "a, c,
e, b, d"; update the comment to reflect "a, c, e, b, d" (and optionally note
that d was inserted as program_id and thus ends up after b due to original
indices) so it matches the assertions in the surrounding code and the
partitioning behavior.

In `@src/processor/fast/delegate_with_actions.rs`:
- Around line 285-290: The copy_from_slice call in delegate_with_actions.rs can
panic because you only check delegate_buffer_account.is_data_empty() but not
that delegated_account and delegate_buffer_account have equal data lengths;
update the code in the block handling delegate_buffer_account and
delegated_account to either resize delegated_account like finalize.rs does (call
the same resize helper before copying) or perform an explicit length check
comparing delegated_account.try_borrow_mut()?.len() (or the account data
lengths) against delegate_buffer_account.try_borrow()?.len() and return a proper
error instead of calling copy_from_slice when sizes differ; specifically modify
the logic around delegate_buffer_account, delegated_account, copy_from_slice and
is_data_empty to ensure lengths match or delegated_account is resized prior to
copying.
- Around line 146-216: Extract the duplicated PDA seed validation into a shared
helper like validate_pda_seeds(seeds: &[Vec<u8>], delegated_account: &Address,
owner_program: &ProgramId) that performs the is_on_curve_fast check, computes
program_id (handling pinocchio_system::ID -> crate::fast::ID), builds the seed
slice for Address::find_program_address, and compares the derived PDA to
delegated_account.address(); replace the large match in delegate_with_actions.rs
(which reads args.delegate.seeds) and the duplicate block in delegate.rs (which
reads args.seeds) with calls to this helper; additionally, change the logic so
zero-length seeds return a clear error (not DlpError::TooManySeeds) and keep
existing error returns ProgramError::InvalidSeeds and DlpError::TooManySeeds
symbols for other invalid cases.

In `@tests/test_delegate_with_actions.rs`:
- Around line 11-13: Add a one-line doc comment above the test_encrypt function
explaining that it is an identity/no-op mock used to exercise the encrypted code
path in tests without performing real cryptography; locate the
test_encrypt(serialized: &[u8]) -> Vec<u8> function and add the brief doc
comment (e.g. "/// No-op identity encrypt used in tests") to make the intent
explicit.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 197ae71 and fe2d14a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .github/workflows/run-tests.yml
  • Cargo.toml
  • src/args/delegate.rs
  • src/args/delegate_with_actions.rs
  • src/args/mod.rs
  • src/compact/account_meta.rs
  • src/compact/instruction.rs
  • src/compact/mod.rs
  • src/discriminator.rs
  • src/encryption/mod.rs
  • src/instruction_builder/delegate_with_actions.rs
  • src/instruction_builder/mod.rs
  • src/lib.rs
  • src/processor/fast/delegate_with_actions.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/utils/requires.rs
  • src/state/utils/try_from_bytes.rs
  • tests/test_delegate_with_actions.rs

@magicblock-labs magicblock-labs deleted a comment from coderabbitai bot Feb 24, 2026
@snawaz snawaz force-pushed the snawaz/delegate-with-actions branch from fe2d14a to 83d61de Compare February 24, 2026 12:08
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (13)
Cargo.toml (1)

74-74: 🧹 Nitpick | 🔵 Trivial

Unbounded solana-sdk version range risks breaking changes.

version = ">=1.16" has no upper bound. Consider adding one to prevent accidental upgrades to incompatible versions (e.g., ">=1.16, <3.0.0"), consistent with the solana-program constraint on Line 52.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 74, The dependency entry for solana-sdk uses an unbounded
range ("solana-sdk = { version = \">=1.16\", optional = true }"); update the
version constraint on the solana-sdk dependency to include an upper bound (for
example ">=1.16, <3.0.0" or matching the solana-program constraint pattern) to
prevent accidental breaking upgrades and keep it consistent with the
solana-program constraint referenced in the Cargo.toml.
.github/workflows/run-tests.yml (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Nightly fmt: consider pinning the nightly version for reproducibility.

An unpinned cargo +nightly fmt means formatting results can drift day-to-day, causing spurious CI failures. Consider pinning to a specific nightly date (e.g., nightly-2025-12-01).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/run-tests.yml at line 54, The CI step invoking the nightly
formatter uses an unpinned toolchain; update the `run` command that currently
calls `rustup component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt
&& cargo +nightly fmt -- --check` to reference a fixed nightly (e.g.,
`nightly-2025-12-01`) in both the `--toolchain` argument and the `cargo +...`
prefix (so replace `--toolchain nightly-x86_64-unknown-linux-gnu` and `cargo
+nightly` with the same pinned `nightly-YYYY-MM-DD` identifier) to ensure
reproducible formatting results.
src/encryption/mod.rs (1)

99-117: ⚠️ Potential issue | 🔴 Critical

XOR stream cipher provides no integrity/authentication — ciphertext is malleable.

This was raised in a previous review and remains unaddressed. The custom xor_with_stream provides confidentiality only. An attacker who can modify the encrypted payload can flip arbitrary plaintext bits without detection, since decrypt will always "succeed" and return silently corrupted data. For delegated actions (where the payload contains instructions that will be executed), this is a serious risk.

The standard fix is to replace the XOR stream with an AEAD scheme (e.g., XChaCha20-Poly1305 via the chacha20poly1305 crate), deriving the key from the DH shared secret via HKDF.

Also applies to: 140-158

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/encryption/mod.rs` around lines 99 - 117, The current
encrypt_with_ephemeral uses xor_with_stream (a malleable stream cipher) and must
be replaced with an AEAD: derive a symmetric key from the X25519 DH shared
secret using HKDF (e.g., sha256) and then use XChaCha20-Poly1305
(chacha20poly1305 crate) to encrypt the plaintext and produce an auth tag;
include the ephemeral public key and the nonce (or use a fixed zero nonce only
if you use XChaCha20 with a unique per-encryption key) in the EncryptedPayloadV1
so the decrypt path can recreate the HKDF key and verify the tag; remove
xor_with_stream usage in encrypt_with_ephemeral and update the corresponding
decrypt implementation to call the AEAD decrypt and return an error on
authentication failure instead of returning silently corrupted data.
src/compact/account_meta.rs (1)

15-16: ⚠️ Potential issue | 🟠 Major

Derived Deserialize bypasses try_new validation on untrusted input.

The inner field is private (good), but the derived Deserialize impl has module-level access and will happily construct an AccountMeta with an index ≥ 64. If this type is ever deserialized from untrusted data (e.g., an on-chain payload), the invariant that index < MAX_PUBKEYS can be silently violated. Consider a custom Deserialize impl that validates the byte, or add a post-deserialization validation step in the consuming code.

💡 Suggested fix: custom Deserialize with validation
-#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug, Serialize)]
 pub struct AccountMeta(u8);
+
+impl<'de> serde::Deserialize<'de> for AccountMeta {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let byte = u8::deserialize(deserializer)?;
+        let index = byte & ACCOUNT_INDEX_MASK;
+        if index >= MAX_PUBKEYS {
+            return Err(serde::de::Error::custom("account index out of range"));
+        }
+        Ok(Self(byte))
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/account_meta.rs` around lines 15 - 16, The derived Deserialize
for AccountMeta bypasses validation; replace it with a custom impl for
serde::Deserialize that reads a u8 (or byte) and calls AccountMeta::try_new (or
checks value < MAX_PUBKEYS) and returns an error on out-of-range values so
deserialization cannot produce an invalid AccountMeta; keep or derive Serialize
but remove Deserialize derive and implement Deserialize for the AccountMeta type
to perform this validation.
src/compact/instruction.rs (2)

19-21: ⚠️ Potential issue | 🟠 Major

Add explicit 6-bit validation for program_id.

Per the established invariant, program_id indexes the same pubkey table as AccountMeta and must be in the range 0..64. The accounts path validates via AccountMeta::try_new(...).expect(...), but program_id trusts the caller's index_of without a guard. Add an assertion to enforce this consistently.

💡 Proposed fix
+        let program_id = index_of(ix.program_id, false);
+        assert!(
+            program_id < compact::MAX_PUBKEYS,
+            "compact program_id index {program_id} must fit in 6 bits (max {})",
+            compact::MAX_PUBKEYS - 1
+        );
         Instruction {
-            program_id: index_of(ix.program_id, false) as u8,
+            program_id,

Based on learnings: "In src/compact/instruction.rs, the program_id is stored as u8 but currently relies on the caller's index_of callback to guarantee it fits in 6 bits (0-63). Add explicit internal validation."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/instruction.rs` around lines 19 - 21, The Instruction
construction trusts index_of(ix.program_id, false) to fit in 6 bits but lacks an
internal guard; update the code that builds the Instruction (the place calling
index_of and creating the Instruction struct) to capture the index into a local
(e.g. let pid = index_of(ix.program_id, false) as usize), assert pid < 64 (or
use debug_assert! if preferred), then cast to u8 when assigning program_id:
Instruction { program_id: pid as u8, ... } so the invariant that program_id is
0..64 is enforced inside this module.

5-10: 🧹 Nitpick | 🔵 Trivial

Consider making struct fields private to enforce invariants.

All fields on Instruction are pub, which allows constructing instances that bypass from_instruction validation entirely. If the 6-bit constraints on program_id and account indices are important invariants, making the fields private and providing accessors would be more robust.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/instruction.rs` around lines 5 - 10, Make the Instruction fields
private to prevent bypassing validation: change the struct so program_id,
accounts, and data are not pub, add public accessors program_id(), accounts(),
data() for read-only access, and provide a validated constructor (e.g.,
Instruction::try_new or from_instruction) that enforces the 6-bit constraint on
program_id (<= 0x3F) and validates account indices per the compact::AccountMeta
rules, returning a Result with an error on violation; keep (or re-derive)
Serialize/Deserialize so serde still works with private fields.
src/args/delegate.rs (1)

2-7: Verify that solana-program has the serde feature enabled.

DelegateArgs contains Option<Pubkey>, which requires the serde feature on solana-program (or solana-pubkey) for Serialize/Deserialize to compile. This was already flagged in a prior review — confirm the Cargo.toml has been updated.

#!/bin/bash
# Check if solana-program dependency has serde feature enabled
rg 'solana-program' Cargo.toml -A 5 | head -20
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/args/delegate.rs` around lines 2 - 7, The Deserialize/Serialize derive on
DelegateArgs (which contains Option<Pubkey>) requires the solana-program (or
solana-pubkey) crate to be compiled with the "serde" feature; update Cargo.toml
to enable serde for the solana-program dependency (or switch to solana-pubkey
with serde enabled) so the derives on DelegateArgs and the Pubkey types compile,
then re-run cargo build to verify; reference the DelegateArgs type and the
Option<Pubkey> field when making the change.
tests/test_delegate_with_actions.rs (1)

60-72: 🧹 Nitpick | 🔵 Trivial

Consider asserting actual compact instruction fields, not just counts.

The roundtrip test verifies signer_count, instruction vector length, and pubkey table bound, but doesn't check individual instruction contents (e.g., program_id index, account meta indices, data bytes). Asserting at least one instruction's fields would catch encoding regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_delegate_with_actions.rs` around lines 60 - 72, The test currently
only checks counts; update the assertions after matching Instructions::ClearText
to validate actual compact instruction fields: inspect the first instruction in
the deserialized instructions vector from args.actions.instructions and assert
its program_id index, account meta indices/order, and data bytes match the
expected encoded values for the roundtrip; also assert at least one
instruction's signer/writable flags (as represented in the compact encoding) and
ensure args.delegate.commit_frequency_ms and args.actions.signer_count remain as
already asserted (use the types DelegateWithActionsArgs,
Instructions::ClearText, and compact::MAX_PUBKEYS to locate the code).
src/processor/fast/delegate_with_actions.rs (3)

285-290: ⚠️ Potential issue | 🟠 Major

copy_from_slice will panic if buffer and delegated account have different data lengths.

If delegate_buffer_account has a different data length than delegated_account, this copy_from_slice call panics at runtime. Unlike finalize.rs which calls resize() before copying, there's no size guard here. Either resize the delegated account to match or add an explicit length check returning a clean error.

Proposed fix — add a length guard
     // Copy the data from the buffer into the original account
     if !delegate_buffer_account.is_data_empty() {
+        let delegate_buffer_data = delegate_buffer_account.try_borrow()?;
         let mut delegated_data = delegated_account.try_borrow_mut()?;
-        let delegate_buffer_data = delegate_buffer_account.try_borrow()?;
+        if delegated_data.len() != delegate_buffer_data.len() {
+            return Err(DlpError::InvalidDataLength.into());
+        }
         (*delegated_data).copy_from_slice(&delegate_buffer_data);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 285 - 290, The
copy_from_slice call in the block that borrows delegate_buffer_account and
delegated_account can panic if their data lengths differ; update the code around
delegate_buffer_account / delegated_account (where try_borrow_mut() and
copy_from_slice are used) to either resize delegated_account to match
delegate_buffer_account (same approach as finalize.rs uses before copying) or
perform an explicit length check comparing delegated_account.data_len() and
delegate_buffer_account.data_len() and return a clean ProgramError if they
differ; ensure you implement the same resize helper or error variant used
elsewhere so the logic mirrors finalize.rs and avoid an unchecked
copy_from_slice.

218-219: 🧹 Nitpick | 🔵 Trivial

Action data is deserialized then re-serialized — consider storing the raw slice.

args.actions was just deserialized from data at line 101, then re-serialized at line 218. If you can compute the byte offset within data, you could write the original bytes directly, saving CU on serialization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 218 - 219,
args.actions is being re-serialized into action_data; instead of
bincode::serialize(&args.actions) in delegate_with_actions.rs, compute the byte
offset where actions were deserialized from the original input slice (`data`
from the earlier deserialization at line ~101) and use that raw subslice as the
action bytes to avoid CPU/compute overhead. Replace the serialize call with
logic that validates the subslice bounds, extracts the actions bytes (use a
slice or clone to a Vec if ownership is required), and preserve the same error
path (return ProgramError::InvalidInstructionData on out-of-bounds or malformed
lengths) so downstream code that expects action_data keeps working.

146-216: 🛠️ Refactor suggestion | 🟠 Major

Extract shared PDA seed validation to reduce duplication with delegate.rs.

This ~70-line seed validation block is duplicated from the existing delegate.rs processor. Additionally, seeds.len() == 0 falls through to the _ => arm returning TooManySeeds, which is misleading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 146 - 216, The
seed-validation block in delegate_with_actions.rs (around is_on_curve_fast,
delegated_account.address(), owner_program.address(), pinocchio_system::ID and
crate::fast::ID) duplicates logic from delegate.rs and mishandles a zero-length
seeds case by falling through to DlpError::TooManySeeds; extract this into a
shared helper (e.g., validate_delegate_pda or find_and_check_pda) that takes
&args.delegate.seeds and the program_id (computed from owner_program.address()),
handle seeds.len() == 0 by returning a clear error (or allowing empty-seed PDAs
if intended), replace the large match with a slice/Vec-based construction used
by Address::find_program_address, and update both delegate_with_actions.rs and
delegate.rs to call the new helper instead of duplicating the match/validation.
src/instruction_builder/delegate_with_actions.rs (2)

114-139: 🧹 Nitpick | 🔵 Trivial

Avoid cloning compact instructions in the cleartext path.

compact_instructions isn’t used after this branch, so you can move it into Instructions::ClearText and skip the extra allocation.

♻️ Proposed refactor
-    } else {
-        Instructions::ClearText {
-            instructions: compact_instructions.clone(),
-        }
-    };
+    } else {
+        Instructions::ClearText {
+            instructions: compact_instructions,
+        }
+    };

Verification (confirm no later uses of compact_instructions after the branch):

#!/bin/bash
rg -n "compact_instructions" src/instruction_builder/delegate_with_actions.rs
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 114 - 139, The
cleartext branch currently clones compact_instructions for
Instructions::ClearText causing an unnecessary allocation; change the branch to
move compact_instructions into Instructions::ClearText (remove .clone()) since
compact_instructions is not used after this point—update the match/if branch
that constructs Instructions::ClearText in delegate_with_actions.rs to take
ownership of compact_instructions and ensure no subsequent code references
compact_instructions after that branch.

278-289: ⚠️ Potential issue | 🟡 Minor

Fix the reorder/mapping comments to match the assertions.

The comment says a, c, e, d, b but the asserts expect a, c, e, b, d; the old→new mapping should reflect the actual pre-reorder indices (program_id d is index 0).

✏️ Suggested fix
-        // reordered: a, c, e, d, b
+        // reordered: a, c, e, b, d
         //            0, 1, 2, 3, 4
@@
-        // old->new mapping: a(0)->0, b(1)->4, c(2)->1, d(3)->3, e(4)->2
+        // old->new mapping: d(0)->4, a(1)->0, c(2)->1, b(3)->3, e(4)->2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 278 - 289, The
inline comments describing the reordered pubkeys and the old->new index mapping
are incorrect; update them to match the assertions in delegate_with_actions.rs
(the assertions on actions.signer_count and actions.pubkeys). Change the
reordered list comment from "a, c, e, d, b" to "a, c, e, b, d" and fix the
old->new mapping so indices reflect the actual pre-reorder positions (noting
program_id `d` is index 0) so the mapping matches the asserted positions used
later when pattern-matching Instructions::ClearText { instructions: ixs }.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/args/delegate_with_actions.rs`:
- Around line 40-48: DecryptedInstructions is dead code; either remove the
struct or make it intentionally used. Option A (preferred): delete the
DecryptedInstructions type and its Serialize/Deserialize derives and
update/remove any imports so tests continue to deserialize directly into
Vec<compact::Instruction>. Option B: keep it—add a clear doc comment describing
its purpose and change the decrypt/test code to deserialize into
DecryptedInstructions (and update call sites to use .instructions and
.random_salt) so the type is actually consumed.

In `@src/compact/account_meta.rs`:
- Around line 18-24: AccountMeta::new and AccountMeta::new_readonly call
Self::try_new(...).expect(...) which panics without showing the offending index;
update these constructors to either return a Result by propagating the TryResult
from try_new or, if you keep the panic API, replace the expect message with one
that includes the invalid index value (e.g. include index in the message) so
callers see which index triggered the error; refer to AccountMeta::new,
AccountMeta::new_readonly, and try_new to apply the change.

In `@src/encryption/mod.rs`:
- Around line 104-107: The code takes the raw X25519 Diffie-Hellman output in
the variable shared (computed from
sender_secret.diffie_hellman(&recipient_public).to_bytes()) and uses it directly
as key material; instead, pass that shared value through an HKDF (e.g.,
hkdf::Hkdf with sha2::Sha256) to derive the actual symmetric key(s) with
explicit domain separation info (a context label like "delegation-program
encrypted-actions v1") and a 32-byte output, then use that derived key for
encryption/MAC/AEAD instead of the raw shared bytes; update the logic around
sender_secret, recipient_public, and shared to call HKDF::new(None, shared) and
hk.expand(...) to produce the final key material.

In `@src/processor/fast/delegate_with_actions.rs`:
- Around line 99-138: After deserializing DelegateWithActionsArgs, add an early
validation that rejects any args.actions.pubkeys whose length exceeds the
compact format limit (compact::MAX_PUBKEYS, value 64) so on-chain validation
matches the compact AccountMeta 6-bit indexing; i.e., in the handler right after
let args: DelegateWithActionsArgs = ... check if args.actions.pubkeys.len() >
compact::MAX_PUBKEYS and return ProgramError::InvalidInstructionData when true
(this should occur before any signer_count, compact index, or remaining_accounts
checks).

---

Duplicate comments:
In @.github/workflows/run-tests.yml:
- Line 54: The CI step invoking the nightly formatter uses an unpinned
toolchain; update the `run` command that currently calls `rustup component add
--toolchain nightly-x86_64-unknown-linux-gnu rustfmt && cargo +nightly fmt --
--check` to reference a fixed nightly (e.g., `nightly-2025-12-01`) in both the
`--toolchain` argument and the `cargo +...` prefix (so replace `--toolchain
nightly-x86_64-unknown-linux-gnu` and `cargo +nightly` with the same pinned
`nightly-YYYY-MM-DD` identifier) to ensure reproducible formatting results.

In `@Cargo.toml`:
- Line 74: The dependency entry for solana-sdk uses an unbounded range
("solana-sdk = { version = \">=1.16\", optional = true }"); update the version
constraint on the solana-sdk dependency to include an upper bound (for example
">=1.16, <3.0.0" or matching the solana-program constraint pattern) to prevent
accidental breaking upgrades and keep it consistent with the solana-program
constraint referenced in the Cargo.toml.

In `@src/args/delegate.rs`:
- Around line 2-7: The Deserialize/Serialize derive on DelegateArgs (which
contains Option<Pubkey>) requires the solana-program (or solana-pubkey) crate to
be compiled with the "serde" feature; update Cargo.toml to enable serde for the
solana-program dependency (or switch to solana-pubkey with serde enabled) so the
derives on DelegateArgs and the Pubkey types compile, then re-run cargo build to
verify; reference the DelegateArgs type and the Option<Pubkey> field when making
the change.

In `@src/compact/account_meta.rs`:
- Around line 15-16: The derived Deserialize for AccountMeta bypasses
validation; replace it with a custom impl for serde::Deserialize that reads a u8
(or byte) and calls AccountMeta::try_new (or checks value < MAX_PUBKEYS) and
returns an error on out-of-range values so deserialization cannot produce an
invalid AccountMeta; keep or derive Serialize but remove Deserialize derive and
implement Deserialize for the AccountMeta type to perform this validation.

In `@src/compact/instruction.rs`:
- Around line 19-21: The Instruction construction trusts index_of(ix.program_id,
false) to fit in 6 bits but lacks an internal guard; update the code that builds
the Instruction (the place calling index_of and creating the Instruction struct)
to capture the index into a local (e.g. let pid = index_of(ix.program_id, false)
as usize), assert pid < 64 (or use debug_assert! if preferred), then cast to u8
when assigning program_id: Instruction { program_id: pid as u8, ... } so the
invariant that program_id is 0..64 is enforced inside this module.
- Around line 5-10: Make the Instruction fields private to prevent bypassing
validation: change the struct so program_id, accounts, and data are not pub, add
public accessors program_id(), accounts(), data() for read-only access, and
provide a validated constructor (e.g., Instruction::try_new or from_instruction)
that enforces the 6-bit constraint on program_id (<= 0x3F) and validates account
indices per the compact::AccountMeta rules, returning a Result with an error on
violation; keep (or re-derive) Serialize/Deserialize so serde still works with
private fields.

In `@src/encryption/mod.rs`:
- Around line 99-117: The current encrypt_with_ephemeral uses xor_with_stream (a
malleable stream cipher) and must be replaced with an AEAD: derive a symmetric
key from the X25519 DH shared secret using HKDF (e.g., sha256) and then use
XChaCha20-Poly1305 (chacha20poly1305 crate) to encrypt the plaintext and produce
an auth tag; include the ephemeral public key and the nonce (or use a fixed zero
nonce only if you use XChaCha20 with a unique per-encryption key) in the
EncryptedPayloadV1 so the decrypt path can recreate the HKDF key and verify the
tag; remove xor_with_stream usage in encrypt_with_ephemeral and update the
corresponding decrypt implementation to call the AEAD decrypt and return an
error on authentication failure instead of returning silently corrupted data.

In `@src/instruction_builder/delegate_with_actions.rs`:
- Around line 114-139: The cleartext branch currently clones
compact_instructions for Instructions::ClearText causing an unnecessary
allocation; change the branch to move compact_instructions into
Instructions::ClearText (remove .clone()) since compact_instructions is not used
after this point—update the match/if branch that constructs
Instructions::ClearText in delegate_with_actions.rs to take ownership of
compact_instructions and ensure no subsequent code references
compact_instructions after that branch.
- Around line 278-289: The inline comments describing the reordered pubkeys and
the old->new index mapping are incorrect; update them to match the assertions in
delegate_with_actions.rs (the assertions on actions.signer_count and
actions.pubkeys). Change the reordered list comment from "a, c, e, d, b" to "a,
c, e, b, d" and fix the old->new mapping so indices reflect the actual
pre-reorder positions (noting program_id `d` is index 0) so the mapping matches
the asserted positions used later when pattern-matching Instructions::ClearText
{ instructions: ixs }.

In `@src/processor/fast/delegate_with_actions.rs`:
- Around line 285-290: The copy_from_slice call in the block that borrows
delegate_buffer_account and delegated_account can panic if their data lengths
differ; update the code around delegate_buffer_account / delegated_account
(where try_borrow_mut() and copy_from_slice are used) to either resize
delegated_account to match delegate_buffer_account (same approach as finalize.rs
uses before copying) or perform an explicit length check comparing
delegated_account.data_len() and delegate_buffer_account.data_len() and return a
clean ProgramError if they differ; ensure you implement the same resize helper
or error variant used elsewhere so the logic mirrors finalize.rs and avoid an
unchecked copy_from_slice.
- Around line 218-219: args.actions is being re-serialized into action_data;
instead of bincode::serialize(&args.actions) in delegate_with_actions.rs,
compute the byte offset where actions were deserialized from the original input
slice (`data` from the earlier deserialization at line ~101) and use that raw
subslice as the action bytes to avoid CPU/compute overhead. Replace the
serialize call with logic that validates the subslice bounds, extracts the
actions bytes (use a slice or clone to a Vec if ownership is required), and
preserve the same error path (return ProgramError::InvalidInstructionData on
out-of-bounds or malformed lengths) so downstream code that expects action_data
keeps working.
- Around line 146-216: The seed-validation block in delegate_with_actions.rs
(around is_on_curve_fast, delegated_account.address(), owner_program.address(),
pinocchio_system::ID and crate::fast::ID) duplicates logic from delegate.rs and
mishandles a zero-length seeds case by falling through to
DlpError::TooManySeeds; extract this into a shared helper (e.g.,
validate_delegate_pda or find_and_check_pda) that takes &args.delegate.seeds and
the program_id (computed from owner_program.address()), handle seeds.len() == 0
by returning a clear error (or allowing empty-seed PDAs if intended), replace
the large match with a slice/Vec-based construction used by
Address::find_program_address, and update both delegate_with_actions.rs and
delegate.rs to call the new helper instead of duplicating the match/validation.

In `@tests/test_delegate_with_actions.rs`:
- Around line 60-72: The test currently only checks counts; update the
assertions after matching Instructions::ClearText to validate actual compact
instruction fields: inspect the first instruction in the deserialized
instructions vector from args.actions.instructions and assert its program_id
index, account meta indices/order, and data bytes match the expected encoded
values for the roundtrip; also assert at least one instruction's signer/writable
flags (as represented in the compact encoding) and ensure
args.delegate.commit_frequency_ms and args.actions.signer_count remain as
already asserted (use the types DelegateWithActionsArgs,
Instructions::ClearText, and compact::MAX_PUBKEYS to locate the code).

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95ca88d and 83d61de.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • .github/workflows/run-tests.yml
  • Cargo.toml
  • src/args/delegate.rs
  • src/args/delegate_with_actions.rs
  • src/args/mod.rs
  • src/compact/account_meta.rs
  • src/compact/instruction.rs
  • src/compact/mod.rs
  • src/discriminator.rs
  • src/encryption/mod.rs
  • src/instruction_builder/delegate_with_actions.rs
  • src/instruction_builder/mod.rs
  • src/lib.rs
  • src/processor/fast/delegate_with_actions.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/utils/requires.rs
  • src/state/utils/try_from_bytes.rs
  • tests/test_delegate_with_actions.rs

@snawaz snawaz force-pushed the snawaz/delegate-with-actions branch from 83d61de to 45ed6a3 Compare February 24, 2026 12:22
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (13)
.github/workflows/run-tests.yml (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Nightly toolchain is still unpinned.

nightly-x86_64-unknown-linux-gnu carries no date suffix, so the formatter version silently changes with each nightly release and can cause spurious CI failures unrelated to code changes. Pin both the rustup installation and the cargo invocation to a specific nightly date for reproducible results.

🔧 Proposed fix
-        run: rustup component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt && cargo +nightly fmt -- --check
+        run: rustup component add --toolchain nightly-2025-12-01-x86_64-unknown-linux-gnu rustfmt && cargo +nightly-2025-12-01 fmt -- --check
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/run-tests.yml at line 54, The workflow installs and uses
an unpinned nightly toolchain string ("nightly-x86_64-unknown-linux-gnu"), which
causes fmt version drift; update the command so both the rustup installation and
the cargo invocation use the same date-pinned nightly toolchain (e.g.,
"nightly-YYYY-MM-DD" with the target triple if you prefer), replacing "rustup
component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt" and "cargo
+nightly fmt -- --check" with the same pinned toolchain token (for example use
"rustup component add --toolchain nightly-YYYY-MM-DD[...]" and "cargo
+nightly-YYYY-MM-DD fmt -- --check") so the formatter version is reproducible.
src/state/utils/try_from_bytes.rs (1)

8-9: ⚠️ Potential issue | 🟡 Minor

Trailing bytes still accepted — fix from 197ae71 appears to have been dropped.

The slice is now correctly bounded to data[8..expected_len] (preventing over-read), but the length guard still uses < rather than !=, so account data larger than 8 + size_of::<Self>() bytes passes validation silently. This was flagged and reportedly fixed in commit 197ae71; the current code reverts that tightening.

🛡️ Proposed fix (exact-length enforcement)
-                if data.len() < expected_len {
+                if data.len() != expected_len {
                     return Err($crate::error::DlpError::InvalidDataLength.into());
                 }

Apply the same change to both try_from_bytes_with_discriminator (line 9) and try_from_bytes_with_discriminator_mut (line 23).

Also applies to: 22-23

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/state/utils/try_from_bytes.rs` around lines 8 - 9, The length check
currently allows trailing bytes because it uses `<` instead of exact equality;
in both functions try_from_bytes_with_discriminator and
try_from_bytes_with_discriminator_mut change the guard from `if data.len() <
expected_len` to `if data.len() != expected_len` (keep the existing bounded
slice `data[8..expected_len]`), so account data must be exactly `8 +
size_of::<Self>()` bytes and any extra or short input is rejected.
Cargo.toml (2)

70-70: ⚠️ Potential issue | 🟡 Minor

Exact serde pin and added derive feature.

The features = ["derive"] addition is correct and required. However, version = "=1.0.226" remains an exact pin that was already flagged in a prior review.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 70, The serde dependency is still pinned exactly which
blocks patch updates; update the serde entry in Cargo.toml (the serde dependency
line) to use a compatible version requirement instead of an exact (=1.0.226)
pin—for example use a caret or range like "1.0" or ">=1.0.226, <2.0" while
keeping features = ["derive"] unchanged so serde can receive
backwards-compatible updates.

74-74: ⚠️ Potential issue | 🟡 Minor

Unbounded solana-sdk version range.

>=1.16 with no upper bound was already flagged in a prior review.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 74, The dependency declaration for solana-sdk in
Cargo.toml uses an unbounded range (>=1.16); update the version constraint for
the solana-sdk dependency to include an explicit upper bound that prevents
automatic adoption of breaking major releases (for example, allow all 1.x
releases but exclude 2.0 and above), keeping the optional = true flag intact;
modify the solana-sdk entry in Cargo.toml accordingly and run cargo update /
cargo metadata to verify the resolved version range.
src/args/delegate.rs (1)

2-7: ⚠️ Potential issue | 🟠 Major

solana-program still missing the serde feature for Pubkey serialization.

DelegateArgs contains Option<Pubkey>, and solana_program::Pubkey only implements Serialize/Deserialize when the serde feature is enabled on solana-program. With version = ">=1.16, <3.0.0", a lock-file update to any 2.x release will break compilation if the feature is absent.

#!/bin/bash
# Verify the resolved solana-program version and whether Pubkey has serde impls
grep -A5 'name = "solana-program"' Cargo.lock | head -20
grep 'features.*serde' Cargo.lock | head -10
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/args/delegate.rs` around lines 2 - 7, DelegateArgs uses Option<Pubkey>
but solana_program::pubkey::Pubkey only implements serde when the "serde"
feature is enabled; fix this by enabling the serde feature for the
solana-program dependency in Cargo.toml (e.g. set solana-program = { version =
">=1.16, <3.0.0", features = ["serde"] }) and then run cargo update to ensure
the lockfile picks up the feature-enabled version so DelegateArgs (and Pubkey)
can derive Serialize/Deserialize successfully.
tests/test_delegate_with_actions.rs (1)

60-71: 🧹 Nitpick | 🔵 Trivial

Strengthen the round‑trip test with decoded content assertions.

The test only checks counts; asserting at least one decoded instruction’s fields (e.g., data bytes and a couple of indices) would better detect encoding regressions.

🧪 Example enhancement
         Instructions::ClearText { instructions } => {
             assert_eq!(instructions.len(), 2);
+            assert_eq!(instructions[0].data, vec![1, 2, 3]);
+            assert_eq!(instructions[1].data, vec![9, 9]);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_delegate_with_actions.rs` around lines 60 - 71, The test currently
only asserts counts after deserializing into DelegateWithActionsArgs and
matching Instructions::ClearText; strengthen it by asserting decoded content for
at least one instruction: after matching Instructions::ClearText { instructions
}, pick instructions[0] (and optionally instructions[1]) and assert key fields
such as the instruction.data byte slice length/contents and account index values
(e.g., instruction.accounts[0] and instruction.accounts[1]) to detect encoding
regressions; update the test around the match on Instructions::ClearText to
include these concrete field assertions.
src/processor/fast/delegate_with_actions.rs (2)

285-289: ⚠️ Potential issue | 🟠 Major

Guard buffer copy against length mismatch.

copy_from_slice will panic if the buffer and delegated account data lengths differ. Add a length check (or resize) before copying.

🛠️ Suggested fix
     if !delegate_buffer_account.is_data_empty() {
-        let mut delegated_data = delegated_account.try_borrow_mut()?;
         let delegate_buffer_data = delegate_buffer_account.try_borrow()?;
+        let mut delegated_data = delegated_account.try_borrow_mut()?;
+        if delegated_data.len() != delegate_buffer_data.len() {
+            return Err(DlpError::InvalidDataLength.into());
+        }
         (*delegated_data).copy_from_slice(&delegate_buffer_data);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 285 - 289, The
current code uses copy_from_slice to copy delegate_buffer_account data into
delegated_account (via delegated_account.try_borrow_mut() and
delegate_buffer_account.try_borrow()), which will panic if their lengths differ;
before copying, check lengths (e.g., compare delegated_data.len() and
delegate_buffer_data.len()) and either return/error if they mismatch or resize
the destination appropriately, and only call copy_from_slice when sizes match to
avoid a panic in the delegate_buffer_account / delegated_account copy path.

99-106: ⚠️ Potential issue | 🟡 Minor

Reject pubkey tables that exceed the compact 6‑bit limit.

Without a MAX_PUBKEYS guard, oversized pubkey tables can be accepted even though compact indices can only address 0..63. Enforce the bound up front.

🛠️ Suggested fix
     let args: DelegateWithActionsArgs = bincode::deserialize(data)
         .map_err(|_| ProgramError::InvalidInstructionData)?;
 
+    if args.actions.pubkeys.len() > crate::compact::MAX_PUBKEYS as usize {
+        return Err(ProgramError::InvalidInstructionData);
+    }
+
     if args.actions.signer_count as usize > args.actions.pubkeys.len() {
         return Err(ProgramError::InvalidInstructionData);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 99 - 106, Add an
explicit bound check to reject oversized pubkey tables that exceed the 6-bit
compact index limit: after deserializing DelegateWithActionsArgs, verify that
args.actions.pubkeys.len() <= MAX_PUBKEYS (define MAX_PUBKEYS = 64 to match
0..63 indices) and return ProgramError::InvalidInstructionData if it exceeds the
limit, placing this check before using args.actions.signer_count and
args.actions.pubkeys.
src/instruction_builder/delegate_with_actions.rs (2)

278-286: ⚠️ Potential issue | 🟡 Minor

Fix the reorder comment to match the asserted order.

The comment says a, c, e, d, b but the assertions expect a, c, e, b, d.

✏️ Suggested fix
-        // reordered: a, c, e, d, b
+        // reordered: a, c, e, b, d
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 278 - 286, The
inline reorder comment is incorrect and should reflect the asserted order;
update the comment above the assertions to "reordered: a, c, e, b, d" (indices
0,1,2,3,4) so it matches the expected values checked by actions.signer_count and
actions.pubkeys[0..4] (a, c, e, b, d).

114-139: 🧹 Nitpick | 🔵 Trivial

Avoid cloning compact instructions in the cleartext path.

You can move compact_instructions into Instructions::ClearText to save an allocation.

♻️ Suggested refactor
-    let compact_payload = if private {
+    let compact_payload = if private {
         let serialized = bincode::serialize(&compact_instructions)
             .expect("compact instruction serialization should not fail");
         let validator = validator
             .expect("delegate.validator is required when private is true");
@@
-    } else {
-        Instructions::ClearText {
-            instructions: compact_instructions.clone(),
-        }
-    };
+    } else {
+        Instructions::ClearText {
+            instructions: compact_instructions,
+        }
+    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 114 - 139, The
cleartext branch unnecessarily clones compact_instructions; instead move it into
Instructions::ClearText to avoid the allocation by using Instructions::ClearText
{ instructions: compact_instructions } (replace the clone). Ensure
compact_instructions is not referenced after this move — if it is needed in the
private branch or later, serialize from a reference before moving or use
std::mem::take to leave an owned value to move; update any code that assumes
compact_instructions remains available. This change affects the compact_payload
construction and the Instructions::ClearText variant handling.
src/compact/instruction.rs (1)

13-22: ⚠️ Potential issue | 🟡 Minor

Validate program_id index against the 6‑bit compact limit.

AccountMeta enforces the 0..63 bound, but program_id currently trusts the caller’s index_of without a local guard. Add an explicit check to keep the invariant self‑contained.

🛠️ Suggested fix
     ) -> Instruction {
+        let program_index = index_of(ix.program_id, false);
+        assert!(
+            program_index < compact::MAX_PUBKEYS,
+            "compact program index must fit in 6 bits"
+        );
         Instruction {
-            program_id: index_of(ix.program_id, false),
+            program_id: program_index,

Based on learnings, the program_id index must be explicitly validated to stay within 0..63 rather than relying solely on the caller’s index_of callback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/instruction.rs` around lines 13 - 22, In from_instruction, don't
trust the caller's index_of for ix.program_id: call index_of(ix.program_id,
false), validate the returned u8 is < 64 (0..63), and if not, panic/unwrap with
a clear message (e.g., "program_id index out of 0..63") so the Instruction
invariant is enforced locally; update the assignment to program_id to use the
validated value before constructing Instruction.
src/encryption/mod.rs (2)

178-212: 🧹 Nitpick | 🔵 Trivial

Add a tamper-detection test once AEAD is in place.

Flip a ciphertext byte and assert decrypt fails to cover integrity checks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/encryption/mod.rs` around lines 178 - 212, Add a tamper-detection unit
test that uses encrypt_with_ephemeral and decrypt to verify AEAD integrity:
create a test (e.g., test_tamper_detection) that generates a validator_secret /
validator_public, encrypts plaintext with encrypt_with_ephemeral, flips one byte
in the resulting ciphertext buffer, then calls decrypt and asserts it returns an
error (i.e., decryption fails). Reference encrypt_with_ephemeral and decrypt
when locating where to add the test so the assertion covers integrity checks
once AEAD is enabled.

17-24: ⚠️ Potential issue | 🟠 Major

Replace the XOR keystream with authenticated encryption + KDF.

Current XOR stream seeded by raw DH output is malleable and offers no integrity; wrong keys/tampering won’t be detected. Derive a symmetric key via HKDF-SHA256 (with a domain-separating info string) and use an AEAD like XChaCha20-Poly1305; store a nonce alongside the ephemeral pubkey in EncryptedPayloadV1, and make decrypt return an error on auth failure.

X25519 HKDF XChaCha20-Poly1305 authenticated encryption best practice Rust

Also applies to: 99-157

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/encryption/mod.rs` around lines 17 - 24, The current XOR keystream must
be replaced with authenticated encryption: update EncryptedPayloadV1 to include
a nonce field (e.g., pub nonce: [u8; 24]) alongside ephemeral_pubkey and
ciphertext, derive a symmetric key from the raw DH shared secret using
HKDF-SHA256 with a domain-separating info string, then encrypt/decrypt using
XChaCha20-Poly1305 (or another AEAD) producing an authentication tag; update the
encrypt path to use HKDF -> AEAD::seal with a generated nonce and store nonce +
ciphertext, and update the decrypt function to perform HKDF -> AEAD::open and
return an explicit error on authentication failure instead of silently returning
garbled plaintext, referencing EncryptedPayloadV1, ephemeral_pubkey, ciphertext,
nonce, and decrypt in your changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Line 75: The Cargo.toml pins the rand dependency with an exact version ("rand
= { version = \"=0.8.5\", features = [\"small_rng\"], optional = true }") which
prevents patch upgrades; change the version specifier to a caret range (e.g.,
"^0.8.5") for that rand entry and apply the identical relaxation to the matching
dev-dependency entry so patch releases (0.8.x) can be picked up while preserving
API compatibility.

In `@Makefile`:
- Around line 7-8: The lint target currently runs "cargo clippy --
--deny=warnings" which skips feature-gated SDK code and test targets; update the
Makefile's lint target (the "lint" recipe) to run clippy across all targets and
with the test/features used in CI/tests by adding the flags "--all-targets" and
the feature set used by tests (e.g. "--features unit_test_config") so clippy
will analyze #[cfg(feature = "sdk")] code and test modules; keep the "--
--deny=warnings" passthrough to preserve fail-on-warning behavior.

---

Duplicate comments:
In @.github/workflows/run-tests.yml:
- Line 54: The workflow installs and uses an unpinned nightly toolchain string
("nightly-x86_64-unknown-linux-gnu"), which causes fmt version drift; update the
command so both the rustup installation and the cargo invocation use the same
date-pinned nightly toolchain (e.g., "nightly-YYYY-MM-DD" with the target triple
if you prefer), replacing "rustup component add --toolchain
nightly-x86_64-unknown-linux-gnu rustfmt" and "cargo +nightly fmt -- --check"
with the same pinned toolchain token (for example use "rustup component add
--toolchain nightly-YYYY-MM-DD[...]" and "cargo +nightly-YYYY-MM-DD fmt --
--check") so the formatter version is reproducible.

In `@Cargo.toml`:
- Line 70: The serde dependency is still pinned exactly which blocks patch
updates; update the serde entry in Cargo.toml (the serde dependency line) to use
a compatible version requirement instead of an exact (=1.0.226) pin—for example
use a caret or range like "1.0" or ">=1.0.226, <2.0" while keeping features =
["derive"] unchanged so serde can receive backwards-compatible updates.
- Line 74: The dependency declaration for solana-sdk in Cargo.toml uses an
unbounded range (>=1.16); update the version constraint for the solana-sdk
dependency to include an explicit upper bound that prevents automatic adoption
of breaking major releases (for example, allow all 1.x releases but exclude 2.0
and above), keeping the optional = true flag intact; modify the solana-sdk entry
in Cargo.toml accordingly and run cargo update / cargo metadata to verify the
resolved version range.

In `@src/args/delegate.rs`:
- Around line 2-7: DelegateArgs uses Option<Pubkey> but
solana_program::pubkey::Pubkey only implements serde when the "serde" feature is
enabled; fix this by enabling the serde feature for the solana-program
dependency in Cargo.toml (e.g. set solana-program = { version = ">=1.16,
<3.0.0", features = ["serde"] }) and then run cargo update to ensure the
lockfile picks up the feature-enabled version so DelegateArgs (and Pubkey) can
derive Serialize/Deserialize successfully.

In `@src/compact/instruction.rs`:
- Around line 13-22: In from_instruction, don't trust the caller's index_of for
ix.program_id: call index_of(ix.program_id, false), validate the returned u8 is
< 64 (0..63), and if not, panic/unwrap with a clear message (e.g., "program_id
index out of 0..63") so the Instruction invariant is enforced locally; update
the assignment to program_id to use the validated value before constructing
Instruction.

In `@src/encryption/mod.rs`:
- Around line 178-212: Add a tamper-detection unit test that uses
encrypt_with_ephemeral and decrypt to verify AEAD integrity: create a test
(e.g., test_tamper_detection) that generates a validator_secret /
validator_public, encrypts plaintext with encrypt_with_ephemeral, flips one byte
in the resulting ciphertext buffer, then calls decrypt and asserts it returns an
error (i.e., decryption fails). Reference encrypt_with_ephemeral and decrypt
when locating where to add the test so the assertion covers integrity checks
once AEAD is enabled.
- Around line 17-24: The current XOR keystream must be replaced with
authenticated encryption: update EncryptedPayloadV1 to include a nonce field
(e.g., pub nonce: [u8; 24]) alongside ephemeral_pubkey and ciphertext, derive a
symmetric key from the raw DH shared secret using HKDF-SHA256 with a
domain-separating info string, then encrypt/decrypt using XChaCha20-Poly1305 (or
another AEAD) producing an authentication tag; update the encrypt path to use
HKDF -> AEAD::seal with a generated nonce and store nonce + ciphertext, and
update the decrypt function to perform HKDF -> AEAD::open and return an explicit
error on authentication failure instead of silently returning garbled plaintext,
referencing EncryptedPayloadV1, ephemeral_pubkey, ciphertext, nonce, and decrypt
in your changes.

In `@src/instruction_builder/delegate_with_actions.rs`:
- Around line 278-286: The inline reorder comment is incorrect and should
reflect the asserted order; update the comment above the assertions to
"reordered: a, c, e, b, d" (indices 0,1,2,3,4) so it matches the expected values
checked by actions.signer_count and actions.pubkeys[0..4] (a, c, e, b, d).
- Around line 114-139: The cleartext branch unnecessarily clones
compact_instructions; instead move it into Instructions::ClearText to avoid the
allocation by using Instructions::ClearText { instructions: compact_instructions
} (replace the clone). Ensure compact_instructions is not referenced after this
move — if it is needed in the private branch or later, serialize from a
reference before moving or use std::mem::take to leave an owned value to move;
update any code that assumes compact_instructions remains available. This change
affects the compact_payload construction and the Instructions::ClearText variant
handling.

In `@src/processor/fast/delegate_with_actions.rs`:
- Around line 285-289: The current code uses copy_from_slice to copy
delegate_buffer_account data into delegated_account (via
delegated_account.try_borrow_mut() and delegate_buffer_account.try_borrow()),
which will panic if their lengths differ; before copying, check lengths (e.g.,
compare delegated_data.len() and delegate_buffer_data.len()) and either
return/error if they mismatch or resize the destination appropriately, and only
call copy_from_slice when sizes match to avoid a panic in the
delegate_buffer_account / delegated_account copy path.
- Around line 99-106: Add an explicit bound check to reject oversized pubkey
tables that exceed the 6-bit compact index limit: after deserializing
DelegateWithActionsArgs, verify that args.actions.pubkeys.len() <= MAX_PUBKEYS
(define MAX_PUBKEYS = 64 to match 0..63 indices) and return
ProgramError::InvalidInstructionData if it exceeds the limit, placing this check
before using args.actions.signer_count and args.actions.pubkeys.

In `@src/state/utils/try_from_bytes.rs`:
- Around line 8-9: The length check currently allows trailing bytes because it
uses `<` instead of exact equality; in both functions
try_from_bytes_with_discriminator and try_from_bytes_with_discriminator_mut
change the guard from `if data.len() < expected_len` to `if data.len() !=
expected_len` (keep the existing bounded slice `data[8..expected_len]`), so
account data must be exactly `8 + size_of::<Self>()` bytes and any extra or
short input is rejected.

In `@tests/test_delegate_with_actions.rs`:
- Around line 60-71: The test currently only asserts counts after deserializing
into DelegateWithActionsArgs and matching Instructions::ClearText; strengthen it
by asserting decoded content for at least one instruction: after matching
Instructions::ClearText { instructions }, pick instructions[0] (and optionally
instructions[1]) and assert key fields such as the instruction.data byte slice
length/contents and account index values (e.g., instruction.accounts[0] and
instruction.accounts[1]) to detect encoding regressions; update the test around
the match on Instructions::ClearText to include these concrete field assertions.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83d61de and 45ed6a3.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • .github/workflows/run-tests.yml
  • Cargo.toml
  • Makefile
  • src/args/delegate.rs
  • src/args/delegate_with_actions.rs
  • src/args/mod.rs
  • src/compact/account_meta.rs
  • src/compact/instruction.rs
  • src/compact/mod.rs
  • src/discriminator.rs
  • src/encryption/mod.rs
  • src/instruction_builder/delegate_with_actions.rs
  • src/instruction_builder/mod.rs
  • src/lib.rs
  • src/processor/fast/delegate_with_actions.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/utils/requires.rs
  • src/state/utils/try_from_bytes.rs
  • tests/test_delegate_with_actions.rs

@snawaz snawaz force-pushed the snawaz/delegate-with-actions branch from 45ed6a3 to 153492d Compare February 24, 2026 12:50
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (15)
src/state/utils/try_from_bytes.rs (1)

8-16: Consider enforcing exact length to avoid accepting trailing bytes.

This is the same concern raised earlier: the < expected_len check permits extra trailing bytes, which relaxes strict size validation. If exact-size account data is intended, switch to != expected_len in both functions.

💡 Suggested fix (exact-length validation)
-                if data.len() < expected_len {
+                if data.len() != expected_len {
                     return Err($crate::error::DlpError::InvalidDataLength.into());
                 }
@@
-                if data.len() < expected_len {
+                if data.len() != expected_len {
                     return Err($crate::error::DlpError::InvalidDataLength.into());
                 }

Also applies to: 22-30

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/state/utils/try_from_bytes.rs` around lines 8 - 16, The length check
currently allows trailing bytes because it only ensures data.len() >=
expected_len; change this to require exact length (data.len() == expected_len)
so extra bytes are rejected; update the check(s) that compute expected_len (the
one around the local expected_len variable and the analogous check in the other
function at lines 22-30) to use equality rather than a less-than comparison,
keeping the existing discriminator check (Self::discriminator().to_bytes()) and
the bytemuck::try_from_bytes::<Self>(&data[8..expected_len]) flow intact.
src/args/delegate_with_actions.rs (1)

40-48: ⚠️ Potential issue | 🟡 Minor

Clarify or remove DecryptedInstructions if it remains unused.

If there’s no consumer, consider removing it or documenting its intended use to avoid dead API surface.

#!/bin/bash
# Find all usages of DecryptedInstructions
rg -n "DecryptedInstructions" --type rust
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/args/delegate_with_actions.rs` around lines 40 - 48, The
DecryptedInstructions struct appears unused; either remove it to avoid dead
public API or document/annotate it if it is intentionally kept: search for
DecryptedInstructions and Instruction usages, and if there are no consumers
delete the DecryptedInstructions declaration and any associated derives to clean
the public surface (and update module exports/tests that referenced it); if it
is intended for future use, add a clear doc comment explaining its purpose and
visibility (or change to pub(crate) or add #[allow(dead_code)]) so its presence
is intentional and won’t trigger dead-API concerns.
Makefile (1)

7-8: 🧹 Nitpick | 🔵 Trivial

Expand clippy coverage to include feature-gated and test code.

The lint target still skips feature-gated SDK code and tests. Consider running clippy with --all-targets and the feature set used in CI/tests to catch issues earlier.

🔧 Proposed update
 lint:
-	cargo clippy -- --deny=warnings
+	cargo clippy --all-targets --features sdk,unit_test_config -- -D warnings
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 7 - 8, The current Makefile lint target only runs
"cargo clippy" and skips feature-gated code and tests; update the "lint"
target's cargo clippy invocation (the line under the lint target) to include
flags that run on all targets and features (e.g., add --all-targets and
--all-features) and keep warnings as denies (e.g., pass -D warnings after the
--) so clippy also checks tests and feature-gated code.
.github/workflows/run-tests.yml (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Pin the nightly toolchain for deterministic formatting.

Unpinned nightly rustfmt can change formatting output day-to-day, causing CI churn.

🔧 Example pin
-        run: rustup component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt && cargo +nightly fmt -- --check
+        run: rustup component add --toolchain nightly-2026-02-01-x86_64-unknown-linux-gnu rustfmt && cargo +nightly-2026-02-01 fmt -- --check
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/run-tests.yml at line 54, The workflow currently installs
and uses an unpinned nightly toolchain via the run step commands "rustup
component add --toolchain nightly-x86_64-unknown-linux-gnu" and "cargo +nightly
fmt -- --check", which causes nondeterministic formatting; pin the nightly to a
specific dateed toolchain (e.g., replace occurrences of
"nightly-x86_64-unknown-linux-gnu" and the "cargo +nightly fmt" invocation with
a concrete pinned toolchain like "nightly-YYYY-MM-DD-x86_64-unknown-linux-gnu"
and use the matching "cargo +nightly-YYYY-MM-DD fmt -- --check") so CI uses a
deterministic rustfmt version.
Cargo.toml (2)

74-74: ⚠️ Potential issue | 🟡 Minor

Add an upper bound to solana-sdk to avoid unintended major upgrades.

The version range is still open-ended and can pull in breaking majors.

🔧 Suggested constraint
-solana-sdk = { version = ">=1.16", optional = true }
+solana-sdk = { version = ">=1.16, <3.0.0", optional = true }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 74, Update the solana-sdk dependency entry to include an
upper bound to prevent accidental major upgrades: change the solana-sdk spec
(the dependency named "solana-sdk" in Cargo.toml) from ">=1.16" to a bounded
range such as ">=1.16, <2.0" (keeping optional = true as-is), then run cargo
update / rebuild to verify dependency resolution.

75-75: ⚠️ Potential issue | 🟡 Minor

Relax the exact rand pin to allow patch updates.

Exact pins block patch-level fixes; a caret keeps compatibility while allowing security updates.

🔧 Suggested constraint
-rand = { version = "=0.8.5", features = ["small_rng"], optional = true }
+rand = { version = "^0.8.5", features = ["small_rng"], optional = true }

Also consider applying the same change to the dev-dependency on Line 80.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 75, The rand dependency is pinned to an exact patch
(=0.8.5), which prevents patch updates; change the version specifier for rand in
Cargo.toml from version = "=0.8.5" to a caret-compatible form like version =
"0.8.5" (or "^0.8.5") so patch-level fixes are allowed, and apply the same
change to the dev-dependency entry for rand (check the [dev-dependencies] rand
entry) so both runtime and dev deps allow patch updates.
src/processor/fast/delegate_with_actions.rs (2)

99-107: ⚠️ Potential issue | 🟡 Minor

Add MAX_PUBKEYS guard to match compact format constraints.

The compact format is limited to 64 pubkeys; accepting more allows unreachable entries and wastes PDA space. Add an early validation after deserialization.

Proposed fix
 use crate::{
     args::{DelegateWithActionsArgs, Instructions},
+    compact,
     consts::{DEFAULT_VALIDATOR_IDENTITY, RENT_EXCEPTION_ZERO_BYTES_LAMPORTS},
     error::DlpError,
     pda,
@@
     let args: DelegateWithActionsArgs = bincode::deserialize(data)
         .map_err(|_| ProgramError::InvalidInstructionData)?;
 
+    if args.actions.pubkeys.len() > compact::MAX_PUBKEYS as usize {
+        return Err(ProgramError::InvalidInstructionData);
+    }
+
     if args.actions.signer_count as usize > args.actions.pubkeys.len() {
         return Err(ProgramError::InvalidInstructionData);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 99 - 107, After
deserializing DelegateWithActionsArgs into args, add an early validation to
enforce the compact-format pubkey limit by checking args.actions.pubkeys.len()
does not exceed MAX_PUBKEYS (and also ensure args.actions.signer_count as usize
<= args.actions.pubkeys.len() as before); if the length is > MAX_PUBKEYS return
ProgramError::InvalidInstructionData. Locate the validation near the current
checks in delegate_with_actions.rs around the deserialization of
DelegateWithActionsArgs and apply the guard referencing args.actions.pubkeys,
args.actions.signer_count, and MAX_PUBKEYS.

285-289: ⚠️ Potential issue | 🟠 Major

Prevent panic on buffer copy length mismatch.

copy_from_slice will panic if the buffer and delegated account data sizes differ. Add a length check (or resize if supported) before copying.

Proposed fix (length check)
     if !delegate_buffer_account.is_data_empty() {
         let mut delegated_data = delegated_account.try_borrow_mut()?;
         let delegate_buffer_data = delegate_buffer_account.try_borrow()?;
+        if delegated_data.len() != delegate_buffer_data.len() {
+            return Err(DlpError::InvalidDataLength.into());
+        }
         (*delegated_data).copy_from_slice(&delegate_buffer_data);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 285 - 289, The
current copy using copy_from_slice can panic if delegate_buffer_account and
delegated_account have different lengths; in the block that checks
!delegate_buffer_account.is_data_empty(), first compare the lengths of
delegated_account.try_borrow_mut()? and delegate_buffer_account.try_borrow()?
(or obtain their data slices) and either return a descriptive error if lengths
differ or copy only the smaller length (e.g., copy min(dest_len, src_len))
instead of calling copy_from_slice on mismatched slices; adjust the logic around
delegated_data, delegate_buffer_data, and the copy_from_slice call to perform
this length check and handle the mismatch safely.
tests/test_delegate_with_actions.rs (1)

26-73: 🧹 Nitpick | 🔵 Trivial

Strengthen roundtrip test by asserting compact instruction contents.

Right now the test only checks counts; asserting at least one compact instruction’s fields (program_id index, account meta flags/indices, and data bytes) will catch encoding regressions.

Suggested add-on assertions (example)
     match args.actions.instructions {
         Instructions::ClearText { instructions } => {
             assert_eq!(instructions.len(), 2);
+            assert_eq!(instructions[0].data, vec![1, 2, 3]);
+            assert_eq!(instructions[1].data, vec![9, 9]);
+            // Consider asserting program_id and account meta indices/flags too.
         }
         Instructions::Encrypted { .. } => {
             panic!("expected cleartext compact instructions");
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_delegate_with_actions.rs` around lines 26 - 73, The test
test_delegate_with_actions_bincode_roundtrip_compact_payload only checks counts;
update it to also validate the decoded compact instruction contents to catch
encoding regressions: after matching Instructions::ClearText in the
DelegateWithActionsArgs (from bincode::deserialize of ix.data[8..]), pick at
least one instruction (e.g., instructions[0]) and assert its program_id index,
its AccountMeta flags/indices for each account, and the instruction data bytes
match the original inputs used when calling delegate_with_actions with
DelegateArgs; keep the existing checks (commit_frequency_ms, signer_count,
length and compact::MAX_PUBKEYS) and add these content assertions to ensure
program_id/account indices and data are encoded and decoded correctly.
src/args/delegate.rs (1)

2-7: ⚠️ Potential issue | 🟠 Major

Verify Pubkey serde support is enabled.

Deriving Serialize/Deserialize for DelegateArgs will only compile if solana-program (or solana-pubkey) is built with the serde feature. Please confirm the dependency features include it; otherwise this is a build break.

#!/bin/bash
# Locate solana-program / solana-pubkey dependency declarations and features.
fd -t f 'Cargo.toml$' -x rg -n 'solana-program|solana-pubkey' -C2 {}

# Check for serde feature usage near those deps.
fd -t f 'Cargo.toml$' -x rg -n 'serde' -C2 {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/args/delegate.rs` around lines 2 - 7, The DelegateArgs derive of
Serialize/Deserialize (and the use of solana_program::pubkey::Pubkey) requires
the solana-program/solana-pubkey crate to be built with its serde feature;
verify and either enable that feature on the dependency or avoid deriving serde
for Pubkey. Concretely, update the dependency declaration for solana-program or
solana-pubkey in Cargo.toml to include features = ["serde"] (or the equivalent
feature name used by that crate), or remove Serialize/Deserialize from the
DelegateArgs derive and provide a serde-compatible wrapper/impl for Pubkey;
refer to the DelegateArgs type and the use of Pubkey in src/args/delegate.rs
when making the change.
src/instruction_builder/delegate_with_actions.rs (3)

136-139: Unnecessary clone in the cleartext branch.

compact_instructions can be moved into Instructions::ClearText rather than cloned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 136 - 139, The
ClearText branch currently clones compact_instructions; instead move it into
Instructions::ClearText to avoid the unnecessary allocation — replace the clone
use with passing compact_instructions directly (i.e., use compact_instructions
rather than compact_instructions.clone()) in the else branch that constructs
Instructions::ClearText.

159-163: .unwrap() on new_index is opaque on failure.

Consider using .expect("reorder_signers_first: missing old_index {old_index}") with context for easier debugging if the invariant is violated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 159 - 163, The
closure new_index uses .unwrap() which yields an opaque panic on failure;
replace .unwrap() with .expect(...) (or similar) and include the old_index value
in the message for context (e.g., "reorder_signers_first: missing old_index
{old_index}") so failures show which index was not found; update the closure
new_index (and any callers inside reorder_signers_first) to use the contextual
expect message.

278-288: ⚠️ Potential issue | 🟡 Minor

Two wrong comments in test_compact_post_delegation_actions.

The "reordered" comment (already flagged) and the old→new mapping comment at line 288 were both copy-pasted from test_reorder_signers_first and are wrong here. In this test, index_of assigns d=0 (program_id), a=1, c=2, b=3, e=4, so the correct mapping is d(0)→4, a(1)→0, c(2)→1, b(3)→3, e(4)→2.

✏️ Suggested fix
-        // reordered: a, c, e, d, b
+        // reordered: a, c, e, b, d
         //            0, 1, 2, 3, 4
         ...
-        // old->new mapping: a(0)->0, b(1)->4, c(2)->1, d(3)->3, e(4)->2
+        // old->new mapping: d(0)->4, a(1)->0, c(2)->1, b(3)->3, e(4)->2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/instruction_builder/delegate_with_actions.rs` around lines 278 - 288, The
comment in test_compact_post_delegation_actions is incorrect: update the
"reordered" and the old->new mapping comments to reflect index_of's assignment
(d=0, a=1, c=2, b=3, e=4) so the mapping reads d(0)→4, a(1)→0, c(2)→1, b(3)→3,
e(4)→2; locate the comments near the assertions in
test_compact_post_delegation_actions and replace the stale copy-pasted mapping
from test_reorder_signers_first with the corrected mapping and reordered
description that matches the assertions and variables a, b, c, d, e and the
index_of usage.
src/encryption/mod.rs (2)

160-212: Tamper-detection test still missing.

A test that flips a ciphertext byte and asserts decryption fails has been flagged previously. With AEAD in place, this would guard against silent acceptance of tampered payloads.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/encryption/mod.rs` around lines 160 - 212, Add a tamper-detection unit
test in the tests module that uses encrypt_with_ephemeral to produce a
ciphertext (using X25519Secret/X25519Public and KEY_LEN like other tests), then
flips or changes a single byte in the ciphertext buffer and calls decrypt with
the same validator secret; assert that decrypt returns an error (i.e.,
decryption fails) rather than producing the original plaintext to ensure AEAD
tamper detection works with encrypt_with_ephemeral and decrypt.

104-138: XOR stream cipher provides no authentication; raw DH output used without KDF.

xor_with_stream XORs the plaintext with a SHA-256-based keystream seeded directly from the raw DH shared secret, providing confidentiality only — ciphertexts are malleable and a wrong key silently produces garbage rather than an error. This is a pre-existing concern and the recommended fix (HKDF-SHA256 + XChaCha20-Poly1305) remains open.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/encryption/mod.rs` around lines 104 - 138, The current encrypt/decrypt
use raw DH bytes with xor_with_stream (seeded by shared) and lack
authentication; change encrypt and decrypt to derive a symmetric AEAD key and
nonce via HKDF-SHA256 from the DH shared secret (include context/info), then
encrypt with XChaCha20-Poly1305 (detached or combined) and verify/authenticate
on decrypt; update EncryptedPayloadV1 to store the ephemeral_pubkey plus the
AEAD ciphertext and any nonce or tag fields, replace xor_with_stream calls in
encrypt/decrypt with AEAD seal/open using the derived key and nonce, and ensure
decrypt returns an error when authentication fails (propagate AEAD errors
instead of producing garbage).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/instruction_builder/delegate_with_actions.rs`:
- Line 29: The public function delegate_with_actions (and its struct field
private: bool) currently can cause a runtime panic when called with private:
true on builds without the sdk feature; update the function's doc comment to add
a "# Panics" section that explicitly states that passing private: true requires
compiling with the "sdk" feature and that calling it without that feature will
panic at runtime (so callers must only pass private: true when the sdk feature
is enabled). Ensure the doc comment is placed on the public
delegate_with_actions API so users see the requirement before calling.

---

Duplicate comments:
In @.github/workflows/run-tests.yml:
- Line 54: The workflow currently installs and uses an unpinned nightly
toolchain via the run step commands "rustup component add --toolchain
nightly-x86_64-unknown-linux-gnu" and "cargo +nightly fmt -- --check", which
causes nondeterministic formatting; pin the nightly to a specific dateed
toolchain (e.g., replace occurrences of "nightly-x86_64-unknown-linux-gnu" and
the "cargo +nightly fmt" invocation with a concrete pinned toolchain like
"nightly-YYYY-MM-DD-x86_64-unknown-linux-gnu" and use the matching "cargo
+nightly-YYYY-MM-DD fmt -- --check") so CI uses a deterministic rustfmt version.

In `@Cargo.toml`:
- Line 74: Update the solana-sdk dependency entry to include an upper bound to
prevent accidental major upgrades: change the solana-sdk spec (the dependency
named "solana-sdk" in Cargo.toml) from ">=1.16" to a bounded range such as
">=1.16, <2.0" (keeping optional = true as-is), then run cargo update / rebuild
to verify dependency resolution.
- Line 75: The rand dependency is pinned to an exact patch (=0.8.5), which
prevents patch updates; change the version specifier for rand in Cargo.toml from
version = "=0.8.5" to a caret-compatible form like version = "0.8.5" (or
"^0.8.5") so patch-level fixes are allowed, and apply the same change to the
dev-dependency entry for rand (check the [dev-dependencies] rand entry) so both
runtime and dev deps allow patch updates.

In `@Makefile`:
- Around line 7-8: The current Makefile lint target only runs "cargo clippy" and
skips feature-gated code and tests; update the "lint" target's cargo clippy
invocation (the line under the lint target) to include flags that run on all
targets and features (e.g., add --all-targets and --all-features) and keep
warnings as denies (e.g., pass -D warnings after the --) so clippy also checks
tests and feature-gated code.

In `@src/args/delegate_with_actions.rs`:
- Around line 40-48: The DecryptedInstructions struct appears unused; either
remove it to avoid dead public API or document/annotate it if it is
intentionally kept: search for DecryptedInstructions and Instruction usages, and
if there are no consumers delete the DecryptedInstructions declaration and any
associated derives to clean the public surface (and update module exports/tests
that referenced it); if it is intended for future use, add a clear doc comment
explaining its purpose and visibility (or change to pub(crate) or add
#[allow(dead_code)]) so its presence is intentional and won’t trigger dead-API
concerns.

In `@src/args/delegate.rs`:
- Around line 2-7: The DelegateArgs derive of Serialize/Deserialize (and the use
of solana_program::pubkey::Pubkey) requires the solana-program/solana-pubkey
crate to be built with its serde feature; verify and either enable that feature
on the dependency or avoid deriving serde for Pubkey. Concretely, update the
dependency declaration for solana-program or solana-pubkey in Cargo.toml to
include features = ["serde"] (or the equivalent feature name used by that
crate), or remove Serialize/Deserialize from the DelegateArgs derive and provide
a serde-compatible wrapper/impl for Pubkey; refer to the DelegateArgs type and
the use of Pubkey in src/args/delegate.rs when making the change.

In `@src/encryption/mod.rs`:
- Around line 160-212: Add a tamper-detection unit test in the tests module that
uses encrypt_with_ephemeral to produce a ciphertext (using
X25519Secret/X25519Public and KEY_LEN like other tests), then flips or changes a
single byte in the ciphertext buffer and calls decrypt with the same validator
secret; assert that decrypt returns an error (i.e., decryption fails) rather
than producing the original plaintext to ensure AEAD tamper detection works with
encrypt_with_ephemeral and decrypt.
- Around line 104-138: The current encrypt/decrypt use raw DH bytes with
xor_with_stream (seeded by shared) and lack authentication; change encrypt and
decrypt to derive a symmetric AEAD key and nonce via HKDF-SHA256 from the DH
shared secret (include context/info), then encrypt with XChaCha20-Poly1305
(detached or combined) and verify/authenticate on decrypt; update
EncryptedPayloadV1 to store the ephemeral_pubkey plus the AEAD ciphertext and
any nonce or tag fields, replace xor_with_stream calls in encrypt/decrypt with
AEAD seal/open using the derived key and nonce, and ensure decrypt returns an
error when authentication fails (propagate AEAD errors instead of producing
garbage).

In `@src/instruction_builder/delegate_with_actions.rs`:
- Around line 136-139: The ClearText branch currently clones
compact_instructions; instead move it into Instructions::ClearText to avoid the
unnecessary allocation — replace the clone use with passing compact_instructions
directly (i.e., use compact_instructions rather than
compact_instructions.clone()) in the else branch that constructs
Instructions::ClearText.
- Around line 159-163: The closure new_index uses .unwrap() which yields an
opaque panic on failure; replace .unwrap() with .expect(...) (or similar) and
include the old_index value in the message for context (e.g.,
"reorder_signers_first: missing old_index {old_index}") so failures show which
index was not found; update the closure new_index (and any callers inside
reorder_signers_first) to use the contextual expect message.
- Around line 278-288: The comment in test_compact_post_delegation_actions is
incorrect: update the "reordered" and the old->new mapping comments to reflect
index_of's assignment (d=0, a=1, c=2, b=3, e=4) so the mapping reads d(0)→4,
a(1)→0, c(2)→1, b(3)→3, e(4)→2; locate the comments near the assertions in
test_compact_post_delegation_actions and replace the stale copy-pasted mapping
from test_reorder_signers_first with the corrected mapping and reordered
description that matches the assertions and variables a, b, c, d, e and the
index_of usage.

In `@src/processor/fast/delegate_with_actions.rs`:
- Around line 99-107: After deserializing DelegateWithActionsArgs into args, add
an early validation to enforce the compact-format pubkey limit by checking
args.actions.pubkeys.len() does not exceed MAX_PUBKEYS (and also ensure
args.actions.signer_count as usize <= args.actions.pubkeys.len() as before); if
the length is > MAX_PUBKEYS return ProgramError::InvalidInstructionData. Locate
the validation near the current checks in delegate_with_actions.rs around the
deserialization of DelegateWithActionsArgs and apply the guard referencing
args.actions.pubkeys, args.actions.signer_count, and MAX_PUBKEYS.
- Around line 285-289: The current copy using copy_from_slice can panic if
delegate_buffer_account and delegated_account have different lengths; in the
block that checks !delegate_buffer_account.is_data_empty(), first compare the
lengths of delegated_account.try_borrow_mut()? and
delegate_buffer_account.try_borrow()? (or obtain their data slices) and either
return a descriptive error if lengths differ or copy only the smaller length
(e.g., copy min(dest_len, src_len)) instead of calling copy_from_slice on
mismatched slices; adjust the logic around delegated_data, delegate_buffer_data,
and the copy_from_slice call to perform this length check and handle the
mismatch safely.

In `@src/state/utils/try_from_bytes.rs`:
- Around line 8-16: The length check currently allows trailing bytes because it
only ensures data.len() >= expected_len; change this to require exact length
(data.len() == expected_len) so extra bytes are rejected; update the check(s)
that compute expected_len (the one around the local expected_len variable and
the analogous check in the other function at lines 22-30) to use equality rather
than a less-than comparison, keeping the existing discriminator check
(Self::discriminator().to_bytes()) and the
bytemuck::try_from_bytes::<Self>(&data[8..expected_len]) flow intact.

In `@tests/test_delegate_with_actions.rs`:
- Around line 26-73: The test
test_delegate_with_actions_bincode_roundtrip_compact_payload only checks counts;
update it to also validate the decoded compact instruction contents to catch
encoding regressions: after matching Instructions::ClearText in the
DelegateWithActionsArgs (from bincode::deserialize of ix.data[8..]), pick at
least one instruction (e.g., instructions[0]) and assert its program_id index,
its AccountMeta flags/indices for each account, and the instruction data bytes
match the original inputs used when calling delegate_with_actions with
DelegateArgs; keep the existing checks (commit_frequency_ms, signer_count,
length and compact::MAX_PUBKEYS) and add these content assertions to ensure
program_id/account indices and data are encoded and decoded correctly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45ed6a3 and 153492d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • .github/workflows/run-tests.yml
  • Cargo.toml
  • Makefile
  • src/args/delegate.rs
  • src/args/delegate_with_actions.rs
  • src/args/mod.rs
  • src/compact/account_meta.rs
  • src/compact/instruction.rs
  • src/compact/mod.rs
  • src/discriminator.rs
  • src/encryption/mod.rs
  • src/instruction_builder/delegate_with_actions.rs
  • src/instruction_builder/mod.rs
  • src/lib.rs
  • src/processor/fast/delegate_with_actions.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/utils/requires.rs
  • src/state/utils/try_from_bytes.rs
  • tests/test_delegate_with_actions.rs

@snawaz snawaz force-pushed the snawaz/delegate-with-actions branch from 153492d to 8edbf20 Compare February 28, 2026 07:35
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

♻️ Duplicate comments (7)
Cargo.toml (2)

81-81: ⚠️ Potential issue | 🟡 Minor

Add an upper bound to solana-sdk to avoid accidental major-version breakage.

At Line 81, >=1.16 is open-ended across majors.

Proposed update
-solana-sdk = { version = ">=1.16", optional = true }
+solana-sdk = { version = ">=1.16, <3.0.0", optional = true }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 81, The solana-sdk dependency version is open-ended
(">=1.16") which can allow accidental breaking major upgrades; update the
version requirement for the solana-sdk dependency in Cargo.toml to include an
upper bound for the next major (for example change the constraint to something
like ">=1.16, <2.0") so it tracks compatible 1.x releases but prevents automatic
2.x upgrades.

82-82: 🧹 Nitpick | 🔵 Trivial

Relax exact rand pin so patch fixes can be consumed.

At Line 82, =0.8.5 blocks 0.8.x patch updates; the same applies to the dev-dependency entry.

Proposed update
-rand = { version = "=0.8.5", features = ["small_rng"], optional = true }
+rand = { version = "^0.8.5", features = ["small_rng"], optional = true }
@@
-rand = { version = "=0.8.5", features = ["small_rng"] }
+rand = { version = "^0.8.5", features = ["small_rng"] }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 82, Change the exact pin on the rand dependency (and the
matching dev-dependency) so patch releases can be consumed: locate the
dependency entry rand = { version = "=0.8.5", features = ["small_rng"], optional
= true } and remove the leading "=" in the version (e.g. version = "0.8.5" or
use a caret like "^0.8.5") and do the same for the rand entry under
dev-dependencies; keep features and optional flags unchanged.
Makefile (1)

7-8: 🧹 Nitpick | 🔵 Trivial

Expand lint coverage to include feature-gated and test targets.

At Line 8, cargo clippy -- --deny=warnings only checks default targets/features, so newly gated paths can be missed.

Proposed update
 lint:
-	cargo clippy -- --deny=warnings
+	cargo clippy --all-targets --all-features -- -D warnings
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 7 - 8, Update the Makefile "lint" recipe (the lint
target) to run clippy across feature-gated and test targets by replacing the
current command `cargo clippy -- --deny=warnings` with a command that includes
the flags --all-targets --all-features --tests (and optionally --workspace) so
clippy runs for tests and feature-gated code, e.g. use `cargo clippy
--all-targets --all-features --tests -- --deny=warnings`.
src/state/utils/try_from_bytes.rs (1)

8-30: ⚠️ Potential issue | 🟠 Major

Restore exact-length validation in zero-copy deserialization.

At Line 9 and Line 23, using < expected_len allows trailing bytes, so malformed oversized buffers can pass.

Proposed fix
-                if data.len() < expected_len {
+                if data.len() != expected_len {
                     return Err($crate::error::DlpError::InvalidDataLength.into());
                 }
@@
-                if data.len() < expected_len {
+                if data.len() != expected_len {
                     return Err($crate::error::DlpError::InvalidDataLength.into());
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/state/utils/try_from_bytes.rs` around lines 8 - 30, The current zero-copy
deserializers try_from_bytes_with_discriminator and
try_from_bytes_with_discriminator_mut allow trailing bytes by checking
data.len() < expected_len; change that validation to require exact length
(data.len() != expected_len) so any oversized buffer fails with
DlpError::InvalidDataLength, leaving the subsequent discriminator check and
bytemuck::try_from_bytes(_mut) slice (data[8..expected_len]) unchanged.
src/compact/instruction.rs (1)

21-21: ⚠️ Potential issue | 🟠 Major

Validate program_id against the same 6-bit bound used by compact account indices.

At Line 21, program_id is accepted unchecked while account indices are explicitly range-validated.

Proposed fix
     ) -> Instruction {
+        let program_id = index_of(ix.program_id, false);
+        assert!(
+            program_id < 64,
+            "compact program index must fit in 6 bits"
+        );
         Instruction {
-            program_id: index_of(ix.program_id, false),
+            program_id,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/instruction.rs` at line 21, program_id is being converted with
index_of(ix.program_id, false) without the same 6-bit range validation applied
to account indices; add the identical 6-bit bound check used for compact account
indices before calling index_of (or modify index_of usage to a validated
variant) and return an error/handle the out-of-range case if program_id exceeds
the 6-bit limit so program_id is rejected the same way account indices are.
.github/workflows/run-tests.yml (1)

54-54: 🧹 Nitpick | 🔵 Trivial

Pin the nightly toolchain used for fmt --check to keep CI deterministic.

At Line 54, floating nightly can change formatter output across runs.

Proposed update
-        run: rustup component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt && cargo +nightly fmt -- --check
+        run: rustup component add --toolchain nightly-2026-02-01-x86_64-unknown-linux-gnu rustfmt && cargo +nightly-2026-02-01 fmt -- --check
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/run-tests.yml at line 54, Pin the nightly toolchain used
for formatting to a fixed date-based release to make CI deterministic: update
the command that currently references "nightly-x86_64-unknown-linux-gnu" and
"cargo +nightly fmt" so both use the same pinned toolchain identifier (for
example "nightly-YYYY-MM-DD-x86_64-unknown-linux-gnu" and "cargo
+nightly-YYYY-MM-DD fmt") ensuring the rustup component add and the cargo
+<toolchain> fmt invocations match exactly.
src/compact/account_meta.rs (1)

19-24: 🧹 Nitpick | 🔵 Trivial

Include the invalid index value in panic paths for easier debugging.

Current panic text ("index is out of range") omits the offending value at Line 20, Line 23, and Line 58.

Proposed improvement
     pub fn new(index: u8, is_signer: bool) -> Self {
-        Self::try_new(index, is_signer, true).expect("index is out of range")
+        Self::try_new(index, is_signer, true).unwrap_or_else(|| {
+            panic!(
+                "compact AccountMeta index {index} is out of range (max {})",
+                MAX_PUBKEYS - 1
+            )
+        })
     }
     pub fn new_readonly(index: u8, is_signer: bool) -> Self {
-        Self::try_new(index, is_signer, false).expect("index is out of range")
+        Self::try_new(index, is_signer, false).unwrap_or_else(|| {
+            panic!(
+                "compact AccountMeta index {index} is out of range (max {})",
+                MAX_PUBKEYS - 1
+            )
+        })
     }
@@
     pub fn set_index(&mut self, new_index: u8) {
         *self = Self::try_new(new_index, self.is_signer(), self.is_writable())
-            .expect("index is out of range");
+            .unwrap_or_else(|| {
+                panic!(
+                    "compact AccountMeta index {new_index} is out of range (max {})",
+                    MAX_PUBKEYS - 1
+                )
+            });
     }

Also applies to: 56-58

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/account_meta.rs` around lines 19 - 24, The existing expect("index
is out of range") calls in new and new_readonly (which call try_new) should
include the offending index value; replace the literal expect(...) usage with an
unwrap_or_else (or similar) that panics with the formatted index, e.g.
unwrap_or_else(|_| panic!("index {} is out of range", index)). Also update the
other panic path that emits "index is out of range" (the panic in try_new or its
callers) to include the index in the message so all occurrences (new,
new_readonly, and the try_new failure path) report the actual invalid index.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/encryption/mod.rs`:
- Line 3: CI reports rustfmt differences in the import line that lists
edwards::CompressedEdwardsY and montgomery::MontgomeryPoint; run `cargo +nightly
fmt` (or `rustfmt`) to reformat the module, ensuring the import list in mod.rs
is wrapped/indented per rustfmt rules so the line ordering/spacing matches the
formatter's output and the commit removes the CI formatting error.
- Around line 98-104: The non-SDK implementation of encrypt_ed25519_recipient
currently panics at runtime; change it to fail at compile-time or to return an
error instead: either remove/guard the function entirely when not(feature =
"sdk") so it is unavailable outside SDK builds, or replace the panic body with a
compile_error! to surface misuse during compilation; if the function signature
must remain for trait compatibility, return a descriptive EncryptionError
variant (e.g., UnsupportedFeature or FeatureNotEnabled) from
encrypt_ed25519_recipient instead of calling panic! so callers receive a
controlled error; reference encrypt_ed25519_recipient, KEY_LEN, and
EncryptionError when making the change.

In `@src/error.rs`:
- Around line 162-170: Remove the misleading commented-out cfg attributes above
the unconditional impl blocks: delete the lines "//#[cfg(not(feature = "sdk"))]"
that appear before the impl From<DlpError> for pinocchio::error::ProgramError
and before impl pinocchio::error::ToStr for DlpError so the file no longer
contains vestigial commented cfg attributes and the impls are clearly
unconditional.

In `@src/instruction_builder/delegate_with_actions.rs`:
- Line 57: Locate the inline TODO comment "// TODO (snawaz): finish it" in
src/instruction_builder/delegate_with_actions.rs and either finish the
incomplete implementation or remove the comment: if there is missing business
logic, implement the remaining behavior in the surrounding function (the
delegate_with_actions builder code) so it fully constructs and returns the
expected delegate-with-actions instruction object; otherwise delete the TODO and
add a short clarifying comment or a unit test that proves the implementation is
complete. Ensure the change updates any related function signatures or tests
that rely on the delegate_with_actions behavior and include a brief commit
message describing the fix.
- Line 54: Replace each empty .expect("") in delegate_with_actions.rs with a
descriptive panic message that identifies the missing validator pubkey and the
operation context; update the four occurrences (the local variable validator at
the first two sites and the validator/withdrawal validator uses at the later
sites) to something like .expect("missing validator pubkey for <operation>:
<brief context>") so failures clearly state which pubkey and which
encryption/signing step failed.
- Around line 255-264: The closure index_of uses unwrap() when searching
non_signers; replace that unwrap() with expect(...) to assert the invariant and
provide a clear debug message: find the position call inside index_of (the
non_signers.iter().position(|ns| &ns.account_meta.pubkey == pk).unwrap()) and
change it to .expect("pubkey not found in non_signers: ensure every instruction
pubkey is added to signers or non_signers") so failures include context
identifying the missing Pubkey and the invariant being relied upon.

In `@src/lib.rs`:
- Around line 4-12: The commented-out mutual-exclusion compile checks for
features `sdk` and `program` in src/lib.rs must be addressed: either restore the
cfg/compile_error blocks (re-enable the checks that enforce exactly one of the
`sdk` or `program` features) or remove those commented lines entirely; if you
deliberately left them disabled for debugging, replace the commented block with
a short TODO comment explaining why and when to re-enable the checks so the
safety guard around `sdk`/`program` feature selection is not left ambiguous.
- Line 12: Run rustfmt (cargo +nightly fmt) to fix the formatting errors
reported by CI and reformat src/lib.rs; specifically remove or correct the stray
commented closing token ");//" (and the similar stray token at the other
occurrence) so the file passes rustfmt, then commit the reformatted file.
- Around line 44-55: The commented-out conditional compilation attributes around
module imports and re-exports (the #[cfg(not(feature = "sdk"))] guards) leave
diff, processor, rkyv (and fast) exposed unconditionally; either remove those
commented lines if you intentionally want these items always public, or restore
the original cfg attributes to restrict them to non-sdk builds. Locate the mod
diff; mod processor; pub use diff::*; pub use rkyv; (and the fast module
mention) and either delete the commented-out attribute lines entirely for a
deliberate API change, or re-enable the attributes by replacing the commented
markers with the original #[cfg(not(feature = "sdk"))] so the symbols are hidden
when the sdk feature is enabled.

In `@tests/test_delegate_with_actions.rs`:
- Around line 127-129: The test function
test_delegate_with_actions_builder_private_sets_encrypted_payload uses SDK-only
APIs (solana_sdk::signature::Keypair and encryption::keypair_to_x25519_secret)
but the #[cfg(feature = "sdk")] attribute is commented out; either re-enable the
feature gate by uncommenting #[cfg(feature = "sdk")] above
test_delegate_with_actions_builder_private_sets_encrypted_payload so it only
compiles when the sdk feature is enabled, or if you intend the test to always
compile, remove SDK-specific usages and replace them with non-SDK-compatible
mocks or stubs; update imports and any references to Keypair and
keypair_to_x25519_secret accordingly to match the chosen approach.

---

Duplicate comments:
In @.github/workflows/run-tests.yml:
- Line 54: Pin the nightly toolchain used for formatting to a fixed date-based
release to make CI deterministic: update the command that currently references
"nightly-x86_64-unknown-linux-gnu" and "cargo +nightly fmt" so both use the same
pinned toolchain identifier (for example
"nightly-YYYY-MM-DD-x86_64-unknown-linux-gnu" and "cargo +nightly-YYYY-MM-DD
fmt") ensuring the rustup component add and the cargo +<toolchain> fmt
invocations match exactly.

In `@Cargo.toml`:
- Line 81: The solana-sdk dependency version is open-ended (">=1.16") which can
allow accidental breaking major upgrades; update the version requirement for the
solana-sdk dependency in Cargo.toml to include an upper bound for the next major
(for example change the constraint to something like ">=1.16, <2.0") so it
tracks compatible 1.x releases but prevents automatic 2.x upgrades.
- Line 82: Change the exact pin on the rand dependency (and the matching
dev-dependency) so patch releases can be consumed: locate the dependency entry
rand = { version = "=0.8.5", features = ["small_rng"], optional = true } and
remove the leading "=" in the version (e.g. version = "0.8.5" or use a caret
like "^0.8.5") and do the same for the rand entry under dev-dependencies; keep
features and optional flags unchanged.

In `@Makefile`:
- Around line 7-8: Update the Makefile "lint" recipe (the lint target) to run
clippy across feature-gated and test targets by replacing the current command
`cargo clippy -- --deny=warnings` with a command that includes the flags
--all-targets --all-features --tests (and optionally --workspace) so clippy runs
for tests and feature-gated code, e.g. use `cargo clippy --all-targets
--all-features --tests -- --deny=warnings`.

In `@src/compact/account_meta.rs`:
- Around line 19-24: The existing expect("index is out of range") calls in new
and new_readonly (which call try_new) should include the offending index value;
replace the literal expect(...) usage with an unwrap_or_else (or similar) that
panics with the formatted index, e.g. unwrap_or_else(|_| panic!("index {} is out
of range", index)). Also update the other panic path that emits "index is out of
range" (the panic in try_new or its callers) to include the index in the message
so all occurrences (new, new_readonly, and the try_new failure path) report the
actual invalid index.

In `@src/compact/instruction.rs`:
- Line 21: program_id is being converted with index_of(ix.program_id, false)
without the same 6-bit range validation applied to account indices; add the
identical 6-bit bound check used for compact account indices before calling
index_of (or modify index_of usage to a validated variant) and return an
error/handle the out-of-range case if program_id exceeds the 6-bit limit so
program_id is rejected the same way account indices are.

In `@src/state/utils/try_from_bytes.rs`:
- Around line 8-30: The current zero-copy deserializers
try_from_bytes_with_discriminator and try_from_bytes_with_discriminator_mut
allow trailing bytes by checking data.len() < expected_len; change that
validation to require exact length (data.len() != expected_len) so any oversized
buffer fails with DlpError::InvalidDataLength, leaving the subsequent
discriminator check and bytemuck::try_from_bytes(_mut) slice
(data[8..expected_len]) unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 153492d and 8edbf20.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • .github/workflows/run-tests.yml
  • Cargo.toml
  • Makefile
  • src/args/delegate.rs
  • src/args/delegate_with_actions.rs
  • src/args/mod.rs
  • src/compact/account_meta.rs
  • src/compact/instruction.rs
  • src/compact/mod.rs
  • src/diff/algorithm.rs
  • src/discriminator.rs
  • src/encryption/mod.rs
  • src/error.rs
  • src/instruction_builder/delegate_with_actions.rs
  • src/instruction_builder/mod.rs
  • src/lib.rs
  • src/processor/fast/delegate_with_actions.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/utils/requires.rs
  • src/state/utils/try_from_bytes.rs
  • tests/test_delegate_with_actions.rs

@snawaz snawaz marked this pull request as ready for review March 11, 2026 14:04
@magicblock-labs magicblock-labs deleted a comment from coderabbitai bot Mar 11, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

♻️ Duplicate comments (3)
Cargo.toml (2)

79-79: ⚠️ Potential issue | 🟡 Minor

Cap solana-sdk to the same major range as solana-program.

Line 79 still uses an open-ended >=1.16 constraint. That can pull a newer solana-sdk major into the workspace while the rest of the Solana stack here is still capped at <3.0.0, which is a brittle mix. Keep this range aligned, and mirror the same cap in the matching dev-dependency entry.

💡 Proposed fix
-solana-sdk = { version = ">=1.16", optional = true }
+solana-sdk = { version = ">=1.16, <3.0.0", optional = true }
#!/bin/bash
for file in $(fd '^Cargo\.toml$'); do
  echo "== $file =="
  rg -n '^\s*solana-(program|sdk)\s*=' "$file"
done

Expected result: every solana-sdk constraint is capped to the same major range as solana-program.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 79, The solana-sdk dependency uses an open-ended
constraint ("solana-sdk = { version = \">=1.16\", optional = true }") which can
pull a newer major than solana-program; update the solana-sdk version spec to
mirror the solana-program major cap (e.g. change to something like ">=1.16,
<3.0.0") and make the same change for any solana-sdk dev-dependency entries so
all solana-* dependencies share the same major-range cap.

80-80: ⚠️ Potential issue | 🟡 Minor

Loosen the exact rand pin so 0.8.x patches can land.

Line 80 uses =0.8.5, which blocks patch-level bug and security fixes for both this optional dependency and the matching dev-dependency. A caret range keeps you on the same 0.8 API line without freezing patch updates.

💡 Proposed fix
-rand = { version = "=0.8.5", features = ["small_rng"], optional = true }
+rand = { version = "^0.8.5", features = ["small_rng"], optional = true }
#!/bin/bash
for file in $(fd '^Cargo\.toml$'); do
  echo "== $file =="
  rg -n '^\s*rand\s*=' "$file"
done

Expected result: no exact =0.8.5 pins remain unless there is a documented ABI reason for freezing patches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 80, Replace the exact pin "rand = { version = \"=0.8.5\",
features = [\"small_rng\"], optional = true }" with a caret range to allow 0.8.x
patch upgrades (for example use "rand = { version = \"^0.8\", features =
[\"small_rng\"], optional = true }"); also scan for and update any matching
dev-dependency exact pins of rand so they use the same caret-range policy to
permit patch fixes while remaining on the 0.8 API.
src/compact/account_meta.rs (1)

31-38: 🧹 Nitpick | 🔵 Trivial

BorshDeserialize accepts invalid index values without validation.

The custom BorshDeserialize implementation deserializes any u8 directly into AccountMeta without validating that the index bits (0-5) are within the valid range. While from_byte would return Some for any value (since masked index is always < 64), explicitly validating on deserialization would catch malformed data early.

💡 Optional: Add validation on deserialization
 impl BorshDeserialize for AccountMeta {
     fn deserialize_reader<R: Read>(
         reader: &mut R,
     ) -> Result<Self, borsh::io::Error> {
         let value = u8::deserialize_reader(reader)?;
-        Ok(Self(value))
+        // Validate the index portion is within bounds
+        // Note: from_byte handles this, but explicit check documents intent
+        Self::from_byte(value).ok_or_else(|| {
+            borsh::io::Error::new(
+                borsh::io::ErrorKind::InvalidData,
+                "invalid compact AccountMeta",
+            )
+        })
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/account_meta.rs` around lines 31 - 38, The custom
BorshDeserialize for AccountMeta currently accepts any u8; update
deserialize_reader (impl BorshDeserialize for AccountMeta) to validate the
deserialized byte before constructing AccountMeta by calling the existing
AccountMeta::from_byte (or reusing its validation logic for index bits 0-5) and
return a borsh::io::Error on invalid/malformed bytes instead of always wrapping
the raw value; ensure the error includes context so deserialization failures are
clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dlp-api/Cargo.toml`:
- Around line 15-16: The Cargo.toml currently pins solana-program to "<3.0.0"
but leaves solana-sdk unbounded, which can cause incompatible Solana major
versions and duplicate types (Pubkey, AccountMeta, EncryptablePubkey) in the
public API; update the solana-sdk dependency to use the same upper bound as
solana-program (e.g., ">=1.16, <3.0.0") so both crates are constrained to the
same major range and avoid type mismatches.

In `@dlp-api/src/encrypt.rs`:
- Around line 85-101: The closure add_to_signers currently uses assert! on
EncryptableAccountMeta invariants which will panic in release; replace those
assert! calls with debug_assert! to keep checks in debug builds (i.e., change
assert!(meta.account_meta.is_signer, ...) and assert!(!meta.is_encryptable, ...)
to debug_assert! macros), and keep the rest of add_to_signers logic unchanged;
alternatively, if you prefer non-panicking behavior, change add_to_signers to
return Result<(), SomeError> and propagate errors instead of asserting (validate
meta.account_meta.is_signer and !meta.is_encryptable and return Err on
violation), updating call sites accordingly.
- Around line 163-172: The closure index_of currently uses unwrap() on
non_signers.iter().position(...), which should be made explicit to document the
invariant; replace that unwrap() call with .expect("index_of: pubkey must appear
in non_signers if not in signers") (or similar) so the failure message clearly
states the assumption, keeping the rest of the logic (casting to u8 and the
signers path) unchanged; target the index_of closure and the position call on
non_signers to implement this.

In `@dlp-api/src/encryption/mod.rs`:
- Around line 42-53: The function ed25519_secret_to_x25519 currently accepts a
loose &[u8] but libsodium-rs requires a 64-byte Ed25519 secret key; change the
signature to ed25519_secret_to_x25519(ed25519_secret_key: &[u8; 64]) ->
Result<[u8; KEY_LEN], EncryptionError>, update the internal call that constructs
crypto_sign::SecretKey::from_bytes to accept the fixed-size reference, and
adjust the single caller keypair_to_x25519_secret to pass its 64-byte
Keypair::to_bytes() result by reference; keep the existing init_sodium() call
and existing error mapping (EncryptionError::InvalidEd25519SecretKey) unchanged.

In `@dlp-api/src/instruction_builder/call_handler_v2.rs`:
- Line 17: The intra-doc link currently uses a private module path
([dlp::processor::call_handler_v2]) which won't resolve from the public dlp-api
docs; update the doc comment in call_handler_v2.rs to either point to a publicly
exported item (e.g., a public re-export like dlp::call_handler_v2 if available)
or render the path as plain code (use backticks around
dlp::processor::call_handler_v2) so the docs build without referencing the
private processor module; target the doc string that contains "See
[dlp::processor::call_handler_v2] for docs." and replace the bracket link
accordingly.

In `@dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs`:
- Around line 16-17: The doc comment at the top of
commit_finalize_from_buffer.rs incorrectly links to
dlp::processor::process_commit_diff_from_buffer; update the reference to the
correct processor function (e.g.
dlp::processor::process_commit_finalize_from_buffer) so the docs match the built
instruction (discriminator commit_finalize_from_buffer) and ensure the doc link
text reflects "commit_finalize_from_buffer" rather than
"commit_diff_from_buffer".
- Around line 49-51: The validator account is incorrectly marked read-only in
the commit_finalize path; update the AccountMeta construction so the validator
is writable by replacing AccountMeta::new_readonly(validator, true) with
AccountMeta::new(validator, true) in the commit_finalize_from_buffer account
list (the snippet containing AccountMeta::new_readonly(validator, true) and
AccountMeta::new(delegated_account, false)); ensure any other commit_finalize
builders under src/instruction_builder follow the same pattern so args.validator
is created with AccountMeta::new(...) when a transfer/lambda-increase path may
touch it.

In `@dlp-api/src/instruction_builder/types/encryptable_types.rs`:
- Around line 122-127: The impl of EncryptableFrom for Vec<u8> (method
encrypted_from producing EncryptableIxData) stores encrypt_begin_offset
verbatim, allowing values > data.len() (e.g., usize::MAX) and breaking the
documented invariant; fix by clamping the provided offset to data.len() (i.e.,
set encrypt_begin_offset = min(offset, self.len())) before constructing
EncryptableIxData so the invariant 0..=data.len() always holds.
- Around line 89-100: The current EncryptableAccountMeta::to_compact method
panics on overflow; change it to a fallible conversion by making to_compact
return a Result<dlp::compact::EncryptableAccountMeta, E> (using your crate's
existing error type or a new builder/validation error) and propagate the error
from dlp::compact::AccountMeta::try_new instead of calling expect; update
callers accordingly (or add a new try_to_compact method with the fallible
signature) so index overflow yields an Err rather than panicking.

In `@src/args/delegate_with_actions.rs`:
- Around line 14-25: The fields inserted_signers and inserted_non_signers in the
PostDelegationActions struct are undocumented; add short doc comments above each
explaining their purpose and semantics (e.g., that they record the number of
signer/non-signer entries inserted by the encryption/packing step so the
decrypt/unpack logic can restore original ordering/indices), mention that they
are initialized to 0 in the encrypt implementation, and note any invariants
(like they must be <= signers.len()/non_signers.len() or used during
decryption). Update the comments directly on the PostDelegationActions struct
fields (inserted_signers and inserted_non_signers) so future readers can
understand their role in encryption/decryption and vector reconstruction.

In `@src/compact/mod.rs`:
- Around line 98-105: The current panic when signers.len() + non_signers.len() >
crate::compact::MAX_PUBKEYS should be converted to a fallible check: change
delegate_with_actions to return Result<PostDelegationActions, Error> (or an
existing crate error type), add an appropriate error variant (e.g.,
TooManyPubkeys or ConstraintViolation) if needed, and replace the panic! with an
early return Err(Error::TooManyPubkeys { max: crate::compact::MAX_PUBKEYS, got:
signers.len() + non_signers.len() }) so callers can handle the constraint
instead of aborting.
- Around line 107-114: The closure index_of currently calls unwrap() on
non_signers.iter().position(...) which can panic if pk is missing; change
index_of to return a Result<u8, _> (e.g., Result<u8, CompactError> or
anyhow::Error) instead of u8, match both signers.iter().position(...) and
non_signers.iter().position(...) and return Err with a clear message if neither
contains pk, then update callers to propagate/handle the error (or map it into
your existing error type) so missing pubkeys are reported explicitly instead of
panicking.
- Around line 48-58: Replace the panic assertions on meta.is_signer with error
returns: instead of assert!(meta.is_signer, ...), have the function return a
Result and return Err(...) carrying a descriptive error (e.g.,
CompactError::InvalidSigner or other existing error type) when meta.is_signer is
false; update the function signature to return Result<_, E> and propagate this
Result to callers. Apply this change for both occurrences that currently use
assert! (the checks around meta.is_signer), and keep the rest of the logic that
finds or pushes into signers (signers.iter_mut().find, found.is_signer,
found.is_writable) intact; ensure callers are updated to handle the new Result
type.

In `@src/processor/delegate_ephemeral_balance.rs`:
- Around line 91-104: Re-derive and validate that the incoming AccountInfo keys
match the expected PDAs before performing the self-CPI: compute
delegate_buffer_pda
(delegate_buffer_pda_from_delegated_account_and_owner_program),
delegation_record_pda (delegation_record_pda_from_delegated_account), and
delegation_metadata_pda (delegation_metadata_pda_from_delegated_account) and
compare each to the corresponding AccountInfo.key passed into the instruction
(e.g., the accounts representing the delegate buffer, delegation record, and
delegation metadata). If any comparison fails, return a deterministic error
(instead of proceeding to the invoke that uses DlpDiscriminator::Delegate and
the serialized delegate_args) so the caller gets a clear local validation
failure. Ensure these checks happen immediately after deriving the PDAs and
before building `data` or calling invoke.

In `@tests/test_cleartext_with_insertable_encrypted.rs`:
- Around line 76-78: The test's heuristic for detecting encryption uses the
closure is_encrypted which checks !ix.data.suffix.as_bytes().is_empty(); add a
short inline comment above this closure explaining that a non-empty data.suffix
is the convention used to mark insertable encrypted instructions in
MaybeEncryptedInstruction, or alternatively implement and call a clear helper
like MaybeEncryptedInstruction::is_encrypted() that encapsulates this check
(referring to the is_encrypted closure and ix.data.suffix symbols so reviewers
can locate and update the logic).

---

Duplicate comments:
In `@Cargo.toml`:
- Line 79: The solana-sdk dependency uses an open-ended constraint ("solana-sdk
= { version = \">=1.16\", optional = true }") which can pull a newer major than
solana-program; update the solana-sdk version spec to mirror the solana-program
major cap (e.g. change to something like ">=1.16, <3.0.0") and make the same
change for any solana-sdk dev-dependency entries so all solana-* dependencies
share the same major-range cap.
- Line 80: Replace the exact pin "rand = { version = \"=0.8.5\", features =
[\"small_rng\"], optional = true }" with a caret range to allow 0.8.x patch
upgrades (for example use "rand = { version = \"^0.8\", features =
[\"small_rng\"], optional = true }"); also scan for and update any matching
dev-dependency exact pins of rand so they use the same caret-range policy to
permit patch fixes while remaining on the 0.8 API.

In `@src/compact/account_meta.rs`:
- Around line 31-38: The custom BorshDeserialize for AccountMeta currently
accepts any u8; update deserialize_reader (impl BorshDeserialize for
AccountMeta) to validate the deserialized byte before constructing AccountMeta
by calling the existing AccountMeta::from_byte (or reusing its validation logic
for index bits 0-5) and return a borsh::io::Error on invalid/malformed bytes
instead of always wrapping the raw value; ensure the error includes context so
deserialization failures are clear.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a629baa6-bb03-45c6-95a5-c5dfdad675b9

📥 Commits

Reviewing files that changed from the base of the PR and between 8edbf20 and b4979e2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (67)
  • Cargo.toml
  • dlp-api/Cargo.toml
  • dlp-api/src/cpi/delegate_with_actions.rs
  • dlp-api/src/cpi/mod.rs
  • dlp-api/src/decrypt.rs
  • dlp-api/src/encrypt.rs
  • dlp-api/src/encryption/mod.rs
  • dlp-api/src/instruction_builder/call_handler.rs
  • dlp-api/src/instruction_builder/call_handler_v2.rs
  • dlp-api/src/instruction_builder/close_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/close_validator_fees_vault.rs
  • dlp-api/src/instruction_builder/commit_diff.rs
  • dlp-api/src/instruction_builder/commit_diff_from_buffer.rs
  • dlp-api/src/instruction_builder/commit_finalize.rs
  • dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs
  • dlp-api/src/instruction_builder/commit_state.rs
  • dlp-api/src/instruction_builder/commit_state_from_buffer.rs
  • dlp-api/src/instruction_builder/delegate.rs
  • dlp-api/src/instruction_builder/delegate_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/delegate_with_actions.rs
  • dlp-api/src/instruction_builder/finalize.rs
  • dlp-api/src/instruction_builder/init_protocol_fees_vault.rs
  • dlp-api/src/instruction_builder/init_validator_fees_vault.rs
  • dlp-api/src/instruction_builder/mod.rs
  • dlp-api/src/instruction_builder/protocol_claim_fees.rs
  • dlp-api/src/instruction_builder/top_up_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/types/encryptable_types.rs
  • dlp-api/src/instruction_builder/types/mod.rs
  • dlp-api/src/instruction_builder/undelegate.rs
  • dlp-api/src/instruction_builder/undelegate_confined_account.rs
  • dlp-api/src/instruction_builder/validator_claim_fees.rs
  • dlp-api/src/instruction_builder/whitelist_validator_for_program.rs
  • dlp-api/src/lib.rs
  • src/args/delegate_with_actions.rs
  • src/compact/account_meta.rs
  • src/compact/instruction.rs
  • src/compact/mod.rs
  • src/lib.rs
  • src/processor/delegate_ephemeral_balance.rs
  • src/processor/fast/delegate_with_actions.rs
  • tests/test_call_handler.rs
  • tests/test_call_handler_v2.rs
  • tests/test_cleartext_with_insertable_encrypted.rs
  • tests/test_close_validator_fees_vault.rs
  • tests/test_commit_fees_on_undelegation.rs
  • tests/test_commit_finalize.rs
  • tests/test_commit_finalize_from_buffer.rs
  • tests/test_commit_on_curve.rs
  • tests/test_commit_state.rs
  • tests/test_commit_state_from_buffer.rs
  • tests/test_commit_state_with_program_config.rs
  • tests/test_commit_undelegate_zero_lamports_system_owned.rs
  • tests/test_delegate_on_curve.rs
  • tests/test_delegate_with_actions.rs
  • tests/test_delegation_confined_accounts.rs
  • tests/test_finalize.rs
  • tests/test_init_fees_vault.rs
  • tests/test_init_validator_fees_vault.rs
  • tests/test_lamports_settlement.rs
  • tests/test_protocol_claim_fees.rs
  • tests/test_top_up.rs
  • tests/test_undelegate.rs
  • tests/test_undelegate_confined_account.rs
  • tests/test_undelegate_on_curve.rs
  • tests/test_undelegate_without_commit.rs
  • tests/test_validator_claim_fees.rs
  • tests/test_whitelist_validator_for_program.rs

@magicblock-labs magicblock-labs deleted a comment from coderabbitai bot Mar 11, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
dlp-api/src/instruction_builder/commit_diff_from_buffer.rs (1)

43-56: ⚠️ Potential issue | 🟠 Major

Change validator account to writable in the instruction builder.

Account 0 is marked readonly, but process_commit_state_internal transfers lamports from the validator account when commit_record_lamports > delegation_record.lamports (line 214-221 of commit_state.rs). A transfer source must be writable. Update the builder:

Required change
-            AccountMeta::new_readonly(validator, true),
+            AccountMeta::new(validator, true),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_diff_from_buffer.rs` around lines 43 -
56, AccountMeta for the validator in the Instruction builder is currently
created as readonly but the runtime (process_commit_state_internal in
commit_state.rs) may transfer lamports from the validator, so change the
validator account to be writable: replace AccountMeta::new_readonly(validator,
true) with AccountMeta::new(validator, true) in the accounts vec in
commit_diff_from_buffer.rs so the validator remains a signer but is writable for
transfers.
src/processor/fast/undelegate.rs (1)

12-32: ⚠️ Potential issue | 🟡 Minor

Run rustfmt on this import block.

CI is already failing on this file for formatting, and this changed section is part of the reported rustfmt diff.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/undelegate.rs` around lines 12 - 32, The import block in
src/processor/fast/undelegate.rs is not rustfmt-formatted; run rustfmt and
reformat the use/import section (e.g., the lines referencing
to_pinocchio_program_error, compute (cfg feature),
crate::consts::{COMMIT_FEE_LAMPORTS, EXTERNAL_UNDELEGATE_DISCRIMINATOR,
SESSION_FEE_LAMPORTS}, crate::error::DlpError, crate::pda, and
crate::processor::fast::utils::pda::{close_pda, close_pda_with_fees,
create_pda}, plus the requires::{...} list) so imports are properly ordered and
wrapped per rustfmt rules, then stage the updated file.
src/processor/fast/delegate.rs (1)

12-29: ⚠️ Potential issue | 🟡 Minor

Run rustfmt on this import block.

This file is already failing the rustfmt check, and the reported diff points at the changed imports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate.rs` around lines 12 - 29, The import block in
delegate.rs is not formatted according to rustfmt; run rustfmt (or cargo fmt) on
src/processor/fast/delegate.rs to reflow and sort the use statements so the
multi-line use tree (items like DelegateArgs, DEFAULT_VALIDATOR_IDENTITY,
RENT_EXCEPTION_ZERO_BYTES_LAMPORTS, DlpError, pda,
processor::fast::{to_pinocchio_program_error, utils::pda::create_pda},
utils::curve::is_on_curve_fast, and the require_* items including
DelegationMetadataCtx and DelegationRecordCtx) is formatted correctly; ensure
the nested braces and commas follow rustfmt style and then re-run tests/CI.
dlp-api/src/instruction_builder/commit_state.rs (1)

42-53: ⚠️ Potential issue | 🔴 Critical

Make the validator account writable in this builder.

process_commit_state can transfer lamports from validator into commit_state_account when args.commit_record_lamports > delegation_record.lamports. With AccountMeta::new_readonly(validator, true), this transfer will fail for callers where the validator is not the transaction fee payer.

Proposed fix
-            AccountMeta::new_readonly(validator, true),
+            AccountMeta::new(validator, true),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_state.rs` around lines 42 - 53, The
validator AccountMeta is currently created as read-only
(AccountMeta::new_readonly(validator, true)) but process_commit_state may
transfer lamports out of validator (when args.commit_record_lamports >
delegation_record.lamports), causing failures for non-fee-payer callers; change
the AccountMeta for the validator in the Instruction builder to a writable
signer (use AccountMeta::new(validator, true) or equivalent) so the validator
account can be debited during the transfer in process_commit_state.
dlp-api/src/instruction_builder/commit_finalize.rs (1)

53-62: ⚠️ Potential issue | 🔴 Critical

Mark both validator and validator_fees_vault writable in this builder.

commit_finalize_internal mutates both accounts: the increase path debits validator, and the decrease path credits validator_fees_vault. Keeping either meta readonly will break one branch of the instruction.

🔧 Proposed fix
         Instruction {
             program_id: dlp::id(),
             accounts: vec![
-                AccountMeta::new_readonly(validator, true),
+                AccountMeta::new(validator, true),
                 AccountMeta::new(delegated_account, false),
                 AccountMeta::new_readonly(delegation_record.0, false),
                 AccountMeta::new(delegation_metadata.0, false),
-                AccountMeta::new_readonly(validator_fees_vault.0, false),
+                AccountMeta::new(validator_fees_vault.0, false),
                 AccountMeta::new_readonly(system_program::id(), false),
             ],

Based on learnings: in src/instruction_builder/commit_finalize.rs, the validator at index 0 must be writable because commit_finalize_internal can transfer from args.validator, and validator_fees_vault is credited on the decrease path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_finalize.rs` around lines 53 - 62, The
Instruction's AccountMeta entries for validator and validator_fees_vault are
currently marked readonly but commit_finalize_internal mutates both; update the
builder so AccountMeta for validator (used as the account at index 0) and
validator_fees_vault (the vault account) are created as writable (i.e., use
AccountMeta::new instead of AccountMeta::new_readonly) so both the increase
(debited validator) and decrease (credited validator_fees_vault) branches
succeed; locate these symbols in commit_finalize.rs where the Instruction is
constructed to change their AccountMeta modes.
src/consts.rs (1)

38-51: ⚠️ Potential issue | 🔴 Critical

Add processor feature to the dlp dependency in dlp-api/Cargo.toml.

DELEGATION_PROGRAM_DATA_ID is gated behind feature = "processor" in src/consts.rs, but dlp-api imports and uses it across multiple instruction builders without enabling that feature on its dlp dependency. This causes a compile-time break when dlp-api is built.

Update dlp-api/Cargo.toml line 12 to:

dlp = { package = "magicblock-delegation-program", path = "..", features = ["processor"] }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/consts.rs` around lines 38 - 51, The build breaks because
DELEGATION_PROGRAM_DATA_ID and BPF_LOADER_UPGRADEABLE_ID are behind feature =
"processor" but the dlp dependency used by dlp-api doesn't enable that feature;
open the dlp-api Cargo.toml and modify the dlp dependency declaration to enable
the "processor" feature (i.e., add features = ["processor"] to the dlp
dependency) so the processor-gated constants are compiled into the dependency
and the instruction builders that reference DELEGATION_PROGRAM_DATA_ID will
resolve.
src/requires.rs (2)

843-859: 🛠️ Refactor suggestion | 🟠 Major

Replace the unsafe pointer cast with safe Address::from() conversion.

The current unsafe { &*(bytes.as_ptr() as *const Address) } couples this code to Address's memory layout unnecessarily. Since Address::from([u8; 32]) is already used elsewhere in this file (line 813), convert the 32-byte slice safely using bytes.try_into() followed by Address::from() to eliminate the unsafe block.

Safer decoding
-            let upgrade_authority_address =
-                unsafe { &*(bytes.as_ptr() as *const Address) };
+            let upgrade_authority_bytes: [u8; 32] = bytes
+                .try_into()
+                .map_err(|_| ProgramError::InvalidAccountData)?;
+            let upgrade_authority_address =
+                Address::from(upgrade_authority_bytes);

             require_eq_keys!(
-                upgrade_authority_address,
+                &upgrade_authority_address,
                 admin.address(),
                 ProgramError::IncorrectAuthority
             );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/requires.rs` around lines 843 - 859, The unsafe pointer cast reading the
upgrade authority bytes should be replaced with a safe conversion: take the
32-byte slice starting at offset_of_upgrade_authority_address+1, call try_into()
to produce a [u8; 32], then construct the address with Address::from(array) and
use that in the require_eq_keys! comparison with admin.address() and
ProgramError::IncorrectAuthority; update the code around program_data /
offset_of_upgrade_authority_address / OPTION_SOME to remove the unsafe block and
use the safe Address::from conversion instead.

435-483: ⚠️ Potential issue | 🟠 Major

Don't expose pub helpers behind a crate-private trait bound.

require_uninitialized_account and require_uninitialized_pda are public functions in a public module, but their ctx parameter is bound by impl RequireUninitializedAccountCtx, which is pub(crate). This creates an unusable public API—downstream crates can see these functions but cannot call them or provide their own context implementations. Either make these helpers pub(crate) or promote the trait and context types to public.

Additionally, in require_authorization (line 851–852), replace the unnecessary unsafe pointer cast with Address::from(&bytes), which is the pattern used throughout the codebase:

let upgrade_authority_address = Address::from(bytes);

Also applies to lines 712–717 and 729–746.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/requires.rs` around lines 435 - 483, The public helper functions
require_uninitialized_account and require_uninitialized_pda are declared pub but
accept a ctx parameter typed as impl RequireUninitializedAccountCtx where
RequireUninitializedAccountCtx is pub(crate), making the API unusable
externally; either change the functions to pub(crate) or make
RequireUninitializedAccountCtx (and any associated context types used by those
functions) public so external callers can implement/provide the ctx. Also
replace the unsafe pointer-to-Address casts in require_authorization (and the
similar sites around the other usages you flagged) with the safe constructor
Address::from(&bytes) (i.e., use Address::from(&bytes) instead of the current
unsafe cast) to match the project pattern.
♻️ Duplicate comments (9)
.github/workflows/run-tests.yml (1)

54-54: ⚠️ Potential issue | 🟡 Minor

Pin the nightly rustfmt toolchain.

Using bare nightly makes this job nondeterministic; upstream rustfmt changes can start failing CI without any repo change. Pin one dated nightly and use it consistently in both rustup and cargo +....

What is the correct rustup and cargo syntax for pinning a specific nightly Rust toolchain date in GitHub Actions, including installing the rustfmt component and invoking `cargo fmt` with that same pinned nightly?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/run-tests.yml at line 54, Replace the unpinned "nightly"
invocations with a specific dated nightly toolchain and use that same identifier
for both rustup and cargo invocations; specifically call rustup component add
--toolchain <YYYY-MM-DD>-x86_64-unknown-linux-gnu rustfmt to install the rustfmt
component for the pinned toolchain and invoke cargo using the exact same pinned
toolchain with cargo +<YYYY-MM-DD> fmt -- --check so both installation and
formatting run against the identical dated nightly.
Makefile (1)

7-8: ⚠️ Potential issue | 🟠 Major

Lint still skips feature-gated and test-only code.

cargo clippy -- --deny=warnings only checks the default target set, so #[cfg(feature = ...)] code and test targets added in this PR can bypass lint entirely. Please mirror the CI feature set here, or lint everything with --all-targets --all-features.

🔧 Proposed fix
 lint:
-	cargo clippy -- --deny=warnings
+	cargo clippy --all-targets --all-features -- -D warnings
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 7 - 8, The lint target currently runs "cargo clippy --
--deny=warnings" which skips feature-gated and test-only code; update the
Makefile's lint target (the "lint" rule invoking "cargo clippy") to run clippy
across all targets and features by adding the flags --all-targets --all-features
(and keep the existing -- --deny=warnings) so CI and local linting mirror each
other.
src/state/utils/try_from_bytes.rs (1)

8-16: ⚠️ Potential issue | 🟠 Major

Require exact-length matches for zero-copy loads.

These guards still allow oversized buffers. Because the code then slices data[8..expected_len], any trailing bytes are discarded before bytemuck sees them, which weakens strict account-data validation.

💡 Proposed fix
-                if data.len() < expected_len {
+                if data.len() != expected_len {
                     return Err($crate::error::DlpError::InvalidDataLength.into());
                 }
@@
-                if data.len() < expected_len {
+                if data.len() != expected_len {
                     return Err($crate::error::DlpError::InvalidDataLength.into());
                 }

Also applies to: 22-30

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/state/utils/try_from_bytes.rs` around lines 8 - 16, The current length
check (if data.len() < expected_len) permits oversized buffers and allows
trailing bytes to be ignored; change the guard to require an exact-length match
(data.len() == expected_len) before slicing and calling bytemuck::try_from_bytes
so any trailing bytes fail validation. Update the same exact-length check in the
other occurrence noted (lines 22-30) so both the discriminator check and the
bytemuck::try_from_bytes call occur only when data.len() equals expected_len,
preserving strict zero-copy validation for the type implementing
Self::discriminator() and the try_from_bytes logic.
src/processor/delegate_ephemeral_balance.rs (1)

91-104: ⚠️ Potential issue | 🟡 Minor

Validate the rederived PDA accounts before invoking Delegate.

The inner instruction is built from delegate_buffer_pda, delegation_record_pda, and delegation_metadata_pda, but the caller-supplied AccountInfos are still passed through unchecked. A mismatched account list now reaches the self-CPI and fails generically instead of returning a local validation error.

Also applies to: 107-119, 121-134

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/delegate_ephemeral_balance.rs` around lines 91 - 104, Re-derive
and validate the PDA addresses before calling Delegate: use
delegate_buffer_pda_from_delegated_account_and_owner_program,
delegation_record_pda_from_delegated_account, and
delegation_metadata_pda_from_delegated_account to compute the expected Pubkeys
and compare them against the caller-supplied AccountInfo.key values (the
accounts you pass into the inner CPI that you build using
DlpDiscriminator::Delegate and args.delegate_args); if any mismatch occurs
return a local error (e.g., InvalidAccountInput or a more specific error)
instead of proceeding to invoke Delegate. Apply the same validation pattern for
the other similar blocks referenced (lines ~107-119 and ~121-134).
dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs (2)

48-51: ⚠️ Potential issue | 🟠 Major

Make the validator meta writable in commit_finalize_from_buffer.

The increase path can debit validator to top up delegated_account. Keeping the validator readonly here only works when the validator is also the fee payer; other callers can fail on the first write.

🔧 Proposed fix
         accounts: vec![
-            AccountMeta::new_readonly(validator, true),
+            AccountMeta::new(validator, true),
             AccountMeta::new(delegated_account, false),

Verify the builder against the internal finalize transfer path; the expected result is a readonly validator meta here and a transfer sourced from args.validator in the processor.

#!/bin/bash
set -euo pipefail

fd 'commit_finalize.rs$' -x sed -n '45,55p'
printf '\n---\n'
fd 'commit_finalize_from_buffer.rs$' -x sed -n '46,56p'
printf '\n---\n'
fd 'commit_finalize_internal.rs$' -x sed -n '118,145p'

Based on learnings: In Solana programs, when a path may invoke a transfer (e.g., from args.validator to args.delegated_account), ensure the validator account (index 0) is explicitly marked writable using AccountMeta::new. Relying on the runtime's fee-payer writability is not a substitute for explicitness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs` around lines
48 - 51, The validator AccountMeta in commit_finalize_from_buffer is currently
created with AccountMeta::new_readonly(validator, true) but the increase path
can debit the validator, so change the meta to be writable by using
AccountMeta::new(validator, true) (i.e., replace new_readonly with new for the
validator entry in the accounts vector) so the builder matches the internal
finalize transfer path that expects args.validator to be writable for transfers.

16-17: ⚠️ Potential issue | 🟡 Minor

Fix the stale commit_diff_from_buffer docs.

This builder emits CommitFinalizeFromBuffer, but the docstring still says "commit state" and points at process_commit_diff_from_buffer.

📝 Proposed fix
-/// Builds a commit state from buffer instruction.
-/// See [dlp::processor::process_commit_diff_from_buffer] for docs.
+/// Builds a commit finalize from buffer instruction.
+/// See [dlp::processor::process_commit_finalize_from_buffer] for docs.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs` around lines
16 - 17, The docstring is stale: update the top comment to state that this
builder creates a CommitFinalizeFromBuffer instruction (not a "commit state")
and fix the reference to point at the correct processor function (replace
dlp::processor::process_commit_diff_from_buffer with
dlp::processor::process_commit_finalize_from_buffer); ensure the doc line
mentions the exact symbol CommitFinalizeFromBuffer so readers can find the
emitted instruction type.
tests/test_cleartext_with_insertable_encrypted.rs (1)

78-80: 🧹 Nitpick | 🔵 Trivial

Consider documenting the encryption detection heuristic.

The is_encrypted closure relies on !ix.data.suffix.as_bytes().is_empty() as the indicator. A brief inline comment explaining this convention would improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_cleartext_with_insertable_encrypted.rs` around lines 78 - 80, The
closure is_encrypted currently uses the heuristic
!ix.data.suffix.as_bytes().is_empty() to detect encrypted instructions; add a
concise inline comment above or beside the closure clarifying that a non-empty
data.suffix indicates an insertable/encrypted payload per the
MaybeEncryptedInstruction convention (explain what suffix contains when
encrypted vs cleartext), so future readers understand why suffix emptiness is
used as the encryption flag.
src/lib.rs (1)

4-12: ⚠️ Potential issue | 🟡 Minor

Fix rustfmt formatting issue flagged by CI.

The pipeline reports a formatting inconsistency at line 12. The commented-out compile_error blocks may have trailing whitespace or formatting issues. Run cargo +nightly fmt to resolve.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib.rs` around lines 4 - 12, The CI formatting error comes from the
commented-out cfg/compile_error blocks (the lines containing #[cfg(...)] and
compile_error!(...)) in lib.rs; remove any trailing whitespace inside those
comment lines and reformat the file by running cargo +nightly fmt (or rustfmt)
so the commented #[cfg(...)] and compile_error! blocks conform to rustfmt rules
and the CI formatting check passes.
src/compact/instruction.rs (1)

19-22: 🧹 Nitpick | 🔵 Trivial

Consider adding explicit validation for program_id index.

While accounts are validated via try_new(...).expect(...) ensuring indices fit in 6 bits, program_id relies solely on the caller's index_of callback to return valid values. For defensive programming, consider adding an assertion:

         Instruction {
-            program_id: index_of(ix.program_id, false),
+            program_id: {
+                let idx = index_of(ix.program_id, false);
+                assert!(idx < compact::MAX_PUBKEYS, "program_id index must fit in 6 bits");
+                idx
+            },

This maintains consistency with the accounts validation and catches bugs in index_of implementations early. Based on learnings, the 6-bit constraint is enforced by the caller, but explicit validation provides defense in depth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/instruction.rs` around lines 19 - 22, Add defensive validation
for the program_id index returned by the index_of callback before constructing
the Instruction: after computing program_id via index_of(ix.program_id, false)
(used to set Instruction.program_id), assert or validate it fits in the same
6-bit range used for accounts (e.g., 0..=63) and return or panic with a clear
error if it does not; mirror the style used by the accounts builder
(try_new(...).expect(...)) so the check sits alongside the existing accounts
validation and references Instruction and index_of by name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dlp-api/Cargo.toml`:
- Line 12: The dlp dependency line currently pulls in the root crate's default
features (transitively building processor and entrypoint); update the dependency
declaration for dlp (the entry "dlp = { package =
\"magicblock-delegation-program\", path = \"..\" }") to disable default features
and only enable the sdk feature by changing it to include default-features =
false and features = ["sdk"] so dlp-api only depends on shared types and diff
serialization.

In `@dlp-api/src/decrypt.rs`:
- Around line 111-131: Clarify the behavior in MaybeEncryptedIxData::decrypt by
adding a short comment inside the decrypt method (near the check
!self.suffix.as_bytes().is_empty()) that explicitly states an empty suffix means
the entire payload is in the cleartext prefix and therefore no decryption is
attempted; reference the MaybeEncryptedIxData::decrypt function and the
prefix/suffix fields so future readers understand why decryption is skipped when
suffix is empty.
- Around line 214-249: Add additional unit tests alongside
test_post_delegation_actions_decrypt_roundtrip to cover edge cases: create tests
for an empty instructions vector (use encrypt and decrypt_with_keypair on vec![]
and assert round-trip yields empty vec), a PostDelegationInstruction with no
accounts (construct with accounts: vec![] and verify encryption/decryption
returns Instruction with empty accounts), and combinations of fully cleartext vs
fully encrypted payloads (e.g., accounts/data all cleartext and all encrypted)
using the same encrypt and decrypt_with_keypair flow and assert expected signers
and decrypted Instruction results; reference the existing
test_post_delegation_actions_decrypt_roundtrip, PostDelegationInstruction,
encrypt, decrypt_with_keypair, AccountMeta, and Instruction to locate where to
add these cases.

In `@dlp-api/src/encrypt.rs`:
- Around line 153-160: The panic in the delegate_with_actions path (the check
using signers.len() + non_signers.len() > dlp::compact::MAX_PUBKEYS) should be
converted to an error return instead: add a new EncryptionError variant (e.g.,
TooManyPubkeys or MaxPubkeysExceeded), change delegate_with_actions (and any
callers) to return Result<..., EncryptionError>, and replace the panic! with an
early Err(EncryptionError::MaxPubkeysExceeded{limit: dlp::compact::MAX_PUBKEYS})
(or similar) so callers can handle the constraint; update propagation sites to
use ? or map_err as appropriate.
- Around line 196-217: The code is panicking because
EncryptablePubkey.encrypt(...) inside the non_signers mapping uses .expect(...);
change this to propagate the error instead: in the construction of
dlp::args::PostDelegationActions (the non_signers field), replace the map that
calls EncryptablePubkey { ... }.encrypt(validator).expect("pubkey encryption
failed") with an approach that returns a Result from the closure (e.g., call
.map(|ns| EncryptablePubkey { ... }.encrypt(validator)) and then collect using
an iterator try-collect (or map and collect::<Result<Vec<_>, _>>()?), so the
function returns Err on encryption failure; ensure the overall function
signature and the Ok((..., signers)) return are consistent with returning
Result<(PostDelegationActions, ...) , E>.
- Around line 173-194: The iterator currently builds compact_instructions with
calls to expect(...) which panic on encryption errors; change the construction
to propagate errors by using fallible iterator combinators (e.g., map to Result
and then collect::<Result<Vec<MaybeEncryptedInstruction>, _>>() or use try_fold)
so the function returns Err instead of panicking. Specifically, for the closure
that creates MaybeEncryptedInstruction (the block referencing
MaybeEncryptedInstruction, index_of(&ix.program_id.pubkey),
meta.to_compact(index).encrypt(validator), and ix.data.encrypt(validator)),
replace the expect calls with propagation (the ? operator) and collect the outer
iterator into Result<Vec<MaybeEncryptedInstruction>, E> before assigning to
compact_instructions so failures from encrypt(...) bubble up.

In `@dlp-api/src/encryption/mod.rs`:
- Around line 64-77: encrypt_ed25519_recipient duplicates the Ed25519→X25519
conversion logic; replace the manual conversion in encrypt_ed25519_recipient
with a call to the existing ed25519_pubkey_to_x25519 function, propagate its
Result (map its error to EncryptionError if necessary), and then use the
returned X25519 public key with crypto_box::seal_box; ensure you handle
init_sodium idempotency by either removing redundant init from
ed25519_pubkey_to_x25519 or accepting the double-init if safe.

In `@dlp-api/src/instruction_builder/types/mod.rs`:
- Around line 22-27: Add doc comments to the Encrypt trait describing the
intended error semantics: document what failures implementations should
represent in type Error (e.g., encryption algorithm failures,
invalid/unsupported Pubkey formats, key validation errors, or I/O/state errors),
and specify whether Error should implement common traits (e.g.,
std::error::Error + Send + Sync) and be stable across versions; annotate the
trait (Encrypt), its associated type Error, and the encrypt(&self, validator:
&Pubkey) method so implementers know which conditions to return Err and what
guarantees callers can expect.

In `@dlp-api/src/lib.rs`:
- Around line 1-9: The CI failed due to rustfmt formatting inconsistencies in
the crate root (lib.rs); run `cargo fmt` locally (or `rustup component add
rustfmt` then `cargo fmt`) to reformat the file containing the exports and
module declarations (the pub use dlp; lines and the pub mod ... / pub use
decrypt::* lines), then verify the changes and commit the updated formatting so
the pipeline passes.

In `@src/compact/account_meta.rs`:
- Around line 89-96: The from_byte function always succeeds because it masks
value with ACCOUNT_INDEX_MASK before calling try_new, so change the signature to
return Self (not Option<Self>) or document why Option is kept; specifically,
either (A) update from_byte to return AccountMeta (or the concrete enum/struct)
instead of Option by calling try_new and unwrapping/constructing directly, or
(B) add a doc comment on from_byte explaining that it returns Option for API
symmetry with try_new and that masking (using ACCOUNT_INDEX_MASK, SIGNER_MASK,
WRITABLE_MASK) guarantees a valid index; reference from_byte, try_new,
ACCOUNT_INDEX_MASK, SIGNER_MASK, and WRITABLE_MASK when making the change.
- Around line 33-40: The BorshDeserialize impl for AccountMeta currently accepts
any u8 without validating the 6-bit index; update deserialize_reader (impl
BorshDeserialize for AccountMeta) to validate the index bits by extracting the
lower 6 bits from the deserialized u8 and either call AccountMeta::try_new (or
otherwise check that index < MAX_PUBKEYS) and return a borsh::io::Error on
invalid index, or if the design intentionally allows all 64 indices, add a clear
comment in the deserialize_reader impl documenting that all 6-bit values (0..63)
are considered valid and therefore no runtime validation is performed.

In `@src/compact/mod.rs`:
- Around line 252-259: The panic on exceeding crate::compact::MAX_PUBKEYS and
the unwrap used to find a position in ClearTextWithInsertable (in
delegate_with_actions and the position lookup near the former unwrap) should be
replaced to mirror ClearText's error handling: remove panic!/unwrap() and return
an appropriate Err variant instead (e.g., a CompactError indicating too many
pubkeys or a missing-position error) from
delegate_with_actions/ClearTextWithInsertable so callers can handle failures;
locate the MAX_PUBKEYS check and the position lookup in ClearTextWithInsertable
and implement the same Result-based error returns and messages used by
ClearText.
- Around line 163-170: Replace the hard panics in the
ClearTextWithInsertable/PostDelegationActions code (the assert! checks on
insertable.inserted_signers and insertable.inserted_non_signers) with the same
approach used in the ClearText implementation: if ClearText uses debug-only
checks, convert these assert! calls to debug_assert!; if ClearText returns a
Result on invariant failure, change the surrounding function(s) to return Result
and return a suitable Err when these signer/non-signer invariants are violated.
Update the checks referencing insertable.inserted_signers and
insertable.inserted_non_signers and propagate the Result-style error up the call
chain if you choose the Result route so behavior matches ClearText.

In `@src/processor/delegate_ephemeral_balance.rs`:
- Around line 107-119: Before calling invoke_signed in
delegate_ephemeral_balance, validate that all accounts requested as writable in
the inner Instruction (payer, ephemeral_balance_account, delegate_buffer_pda,
delegation_record_pda, delegation_metadata_pda) were actually granted writable
by the outer call; use the existing helpers (e.g., load_pda or require_writable
from loaders.rs/requires.rs) to assert account.is_writable for each
corresponding AccountInfo and return a clear error if not, and apply the same
checks for the second CPI block covering the accounts in the 121-134 region
before its invoke_signed.

In `@src/processor/fast/finalize.rs`:
- Around line 11-18: The import list in finalize.rs is not formatted per rustfmt
(CI failing); run rustfmt (e.g. cargo +nightly fmt or rustfmt on this file) to
reflow the use/import block so items like processor::fast::utils::pda::close_pda
and the requires::{...} group are formatted/sorted correctly; ensure the use
block containing close_pda and require_* symbols is cleaned up by rustfmt and
then re-run cargo +nightly fmt -- --check to confirm passes.

In `@src/processor/fast/undelegate_confined_account.rs`:
- Around line 10-17: The import block in undelegate_confined_account.rs is
misformatted and failing rustfmt in CI; reformat the imports (preserving the
same symbols like processor::fast::utils::pda::{close_pda, create_pda} and the
requires::{require_authorization, require_initialized_delegation_metadata,
require_initialized_delegation_record, require_owned_pda, require_signer,
require_uninitialized_pda, UndelegateBufferCtx}) so they conform to rustfmt
(e.g., collapse/align braces and list items per rustfmt rules) and then run
rustfmt (cargo fmt or rustfmt) on the file before pushing the change.

In `@tests/integration/programs/test-delegation/Cargo.toml`:
- Line 22: Remove the machine-specific checked-in local path override for the
ephemeral-rollups-sdk dependency (the commented line referencing
ephemeral-rollups-sdk with path "/Users/...") from the Cargo.toml; either delete
that line or replace it with a generic dependency entry (or document the local
path override in contributor docs or use a .cargo/config [patch.crates-io]
override) so the manifest does not contain absolute, machine-specific paths.

In `@tests/test_call_handler.rs`:
- Around line 256-261: The test currently calls ProgramTest::new with a native
processor (processor!(dlp::slow_process_instruction)) but also sets
prefer_bpf(true), which causes the BPF .so to be used and the native slow
processor to be bypassed; either remove the prefer_bpf(true) call so ProgramTest
will run the supplied native processor dlp::slow_process_instruction, or remove
the processor!(dlp::slow_process_instruction) argument and rely on
prefer_bpf(true) to test the compiled BPF artifact—pick the intended execution
path and update the ProgramTest invocation accordingly.

---

Outside diff comments:
In `@dlp-api/src/instruction_builder/commit_diff_from_buffer.rs`:
- Around line 43-56: AccountMeta for the validator in the Instruction builder is
currently created as readonly but the runtime (process_commit_state_internal in
commit_state.rs) may transfer lamports from the validator, so change the
validator account to be writable: replace AccountMeta::new_readonly(validator,
true) with AccountMeta::new(validator, true) in the accounts vec in
commit_diff_from_buffer.rs so the validator remains a signer but is writable for
transfers.

In `@dlp-api/src/instruction_builder/commit_finalize.rs`:
- Around line 53-62: The Instruction's AccountMeta entries for validator and
validator_fees_vault are currently marked readonly but commit_finalize_internal
mutates both; update the builder so AccountMeta for validator (used as the
account at index 0) and validator_fees_vault (the vault account) are created as
writable (i.e., use AccountMeta::new instead of AccountMeta::new_readonly) so
both the increase (debited validator) and decrease (credited
validator_fees_vault) branches succeed; locate these symbols in
commit_finalize.rs where the Instruction is constructed to change their
AccountMeta modes.

In `@dlp-api/src/instruction_builder/commit_state.rs`:
- Around line 42-53: The validator AccountMeta is currently created as read-only
(AccountMeta::new_readonly(validator, true)) but process_commit_state may
transfer lamports out of validator (when args.commit_record_lamports >
delegation_record.lamports), causing failures for non-fee-payer callers; change
the AccountMeta for the validator in the Instruction builder to a writable
signer (use AccountMeta::new(validator, true) or equivalent) so the validator
account can be debited during the transfer in process_commit_state.

In `@src/consts.rs`:
- Around line 38-51: The build breaks because DELEGATION_PROGRAM_DATA_ID and
BPF_LOADER_UPGRADEABLE_ID are behind feature = "processor" but the dlp
dependency used by dlp-api doesn't enable that feature; open the dlp-api
Cargo.toml and modify the dlp dependency declaration to enable the "processor"
feature (i.e., add features = ["processor"] to the dlp dependency) so the
processor-gated constants are compiled into the dependency and the instruction
builders that reference DELEGATION_PROGRAM_DATA_ID will resolve.

In `@src/processor/fast/delegate.rs`:
- Around line 12-29: The import block in delegate.rs is not formatted according
to rustfmt; run rustfmt (or cargo fmt) on src/processor/fast/delegate.rs to
reflow and sort the use statements so the multi-line use tree (items like
DelegateArgs, DEFAULT_VALIDATOR_IDENTITY, RENT_EXCEPTION_ZERO_BYTES_LAMPORTS,
DlpError, pda, processor::fast::{to_pinocchio_program_error,
utils::pda::create_pda}, utils::curve::is_on_curve_fast, and the require_* items
including DelegationMetadataCtx and DelegationRecordCtx) is formatted correctly;
ensure the nested braces and commas follow rustfmt style and then re-run
tests/CI.

In `@src/processor/fast/undelegate.rs`:
- Around line 12-32: The import block in src/processor/fast/undelegate.rs is not
rustfmt-formatted; run rustfmt and reformat the use/import section (e.g., the
lines referencing to_pinocchio_program_error, compute (cfg feature),
crate::consts::{COMMIT_FEE_LAMPORTS, EXTERNAL_UNDELEGATE_DISCRIMINATOR,
SESSION_FEE_LAMPORTS}, crate::error::DlpError, crate::pda, and
crate::processor::fast::utils::pda::{close_pda, close_pda_with_fees,
create_pda}, plus the requires::{...} list) so imports are properly ordered and
wrapped per rustfmt rules, then stage the updated file.

In `@src/requires.rs`:
- Around line 843-859: The unsafe pointer cast reading the upgrade authority
bytes should be replaced with a safe conversion: take the 32-byte slice starting
at offset_of_upgrade_authority_address+1, call try_into() to produce a [u8; 32],
then construct the address with Address::from(array) and use that in the
require_eq_keys! comparison with admin.address() and
ProgramError::IncorrectAuthority; update the code around program_data /
offset_of_upgrade_authority_address / OPTION_SOME to remove the unsafe block and
use the safe Address::from conversion instead.
- Around line 435-483: The public helper functions require_uninitialized_account
and require_uninitialized_pda are declared pub but accept a ctx parameter typed
as impl RequireUninitializedAccountCtx where RequireUninitializedAccountCtx is
pub(crate), making the API unusable externally; either change the functions to
pub(crate) or make RequireUninitializedAccountCtx (and any associated context
types used by those functions) public so external callers can implement/provide
the ctx. Also replace the unsafe pointer-to-Address casts in
require_authorization (and the similar sites around the other usages you
flagged) with the safe constructor Address::from(&bytes) (i.e., use
Address::from(&bytes) instead of the current unsafe cast) to match the project
pattern.

---

Duplicate comments:
In @.github/workflows/run-tests.yml:
- Line 54: Replace the unpinned "nightly" invocations with a specific dated
nightly toolchain and use that same identifier for both rustup and cargo
invocations; specifically call rustup component add --toolchain
<YYYY-MM-DD>-x86_64-unknown-linux-gnu rustfmt to install the rustfmt component
for the pinned toolchain and invoke cargo using the exact same pinned toolchain
with cargo +<YYYY-MM-DD> fmt -- --check so both installation and formatting run
against the identical dated nightly.

In `@dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs`:
- Around line 48-51: The validator AccountMeta in commit_finalize_from_buffer is
currently created with AccountMeta::new_readonly(validator, true) but the
increase path can debit the validator, so change the meta to be writable by
using AccountMeta::new(validator, true) (i.e., replace new_readonly with new for
the validator entry in the accounts vector) so the builder matches the internal
finalize transfer path that expects args.validator to be writable for transfers.
- Around line 16-17: The docstring is stale: update the top comment to state
that this builder creates a CommitFinalizeFromBuffer instruction (not a "commit
state") and fix the reference to point at the correct processor function
(replace dlp::processor::process_commit_diff_from_buffer with
dlp::processor::process_commit_finalize_from_buffer); ensure the doc line
mentions the exact symbol CommitFinalizeFromBuffer so readers can find the
emitted instruction type.

In `@Makefile`:
- Around line 7-8: The lint target currently runs "cargo clippy --
--deny=warnings" which skips feature-gated and test-only code; update the
Makefile's lint target (the "lint" rule invoking "cargo clippy") to run clippy
across all targets and features by adding the flags --all-targets --all-features
(and keep the existing -- --deny=warnings) so CI and local linting mirror each
other.

In `@src/compact/instruction.rs`:
- Around line 19-22: Add defensive validation for the program_id index returned
by the index_of callback before constructing the Instruction: after computing
program_id via index_of(ix.program_id, false) (used to set
Instruction.program_id), assert or validate it fits in the same 6-bit range used
for accounts (e.g., 0..=63) and return or panic with a clear error if it does
not; mirror the style used by the accounts builder (try_new(...).expect(...)) so
the check sits alongside the existing accounts validation and references
Instruction and index_of by name.

In `@src/lib.rs`:
- Around line 4-12: The CI formatting error comes from the commented-out
cfg/compile_error blocks (the lines containing #[cfg(...)] and
compile_error!(...)) in lib.rs; remove any trailing whitespace inside those
comment lines and reformat the file by running cargo +nightly fmt (or rustfmt)
so the commented #[cfg(...)] and compile_error! blocks conform to rustfmt rules
and the CI formatting check passes.

In `@src/processor/delegate_ephemeral_balance.rs`:
- Around line 91-104: Re-derive and validate the PDA addresses before calling
Delegate: use delegate_buffer_pda_from_delegated_account_and_owner_program,
delegation_record_pda_from_delegated_account, and
delegation_metadata_pda_from_delegated_account to compute the expected Pubkeys
and compare them against the caller-supplied AccountInfo.key values (the
accounts you pass into the inner CPI that you build using
DlpDiscriminator::Delegate and args.delegate_args); if any mismatch occurs
return a local error (e.g., InvalidAccountInput or a more specific error)
instead of proceeding to invoke Delegate. Apply the same validation pattern for
the other similar blocks referenced (lines ~107-119 and ~121-134).

In `@src/state/utils/try_from_bytes.rs`:
- Around line 8-16: The current length check (if data.len() < expected_len)
permits oversized buffers and allows trailing bytes to be ignored; change the
guard to require an exact-length match (data.len() == expected_len) before
slicing and calling bytemuck::try_from_bytes so any trailing bytes fail
validation. Update the same exact-length check in the other occurrence noted
(lines 22-30) so both the discriminator check and the bytemuck::try_from_bytes
call occur only when data.len() equals expected_len, preserving strict zero-copy
validation for the type implementing Self::discriminator() and the
try_from_bytes logic.

In `@tests/test_cleartext_with_insertable_encrypted.rs`:
- Around line 78-80: The closure is_encrypted currently uses the heuristic
!ix.data.suffix.as_bytes().is_empty() to detect encrypted instructions; add a
concise inline comment above or beside the closure clarifying that a non-empty
data.suffix indicates an insertable/encrypted payload per the
MaybeEncryptedInstruction convention (explain what suffix contains when
encrypted vs cleartext), so future readers understand why suffix emptiness is
used as the encryption flag.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 021ea59a-b035-4aba-a0d2-0b96dc06c96e

📥 Commits

Reviewing files that changed from the base of the PR and between 95ca88d and cb2b798.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • tests/integration/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (85)
  • .coderabbit.yaml
  • .github/workflows/run-tests.yml
  • Cargo.toml
  • Makefile
  • dlp-api/Cargo.toml
  • dlp-api/src/cpi/delegate_with_actions.rs
  • dlp-api/src/cpi/mod.rs
  • dlp-api/src/decrypt.rs
  • dlp-api/src/encrypt.rs
  • dlp-api/src/encryption/mod.rs
  • dlp-api/src/instruction_builder/call_handler.rs
  • dlp-api/src/instruction_builder/call_handler_v2.rs
  • dlp-api/src/instruction_builder/close_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/close_validator_fees_vault.rs
  • dlp-api/src/instruction_builder/commit_diff.rs
  • dlp-api/src/instruction_builder/commit_diff_from_buffer.rs
  • dlp-api/src/instruction_builder/commit_finalize.rs
  • dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs
  • dlp-api/src/instruction_builder/commit_state.rs
  • dlp-api/src/instruction_builder/commit_state_from_buffer.rs
  • dlp-api/src/instruction_builder/delegate.rs
  • dlp-api/src/instruction_builder/delegate_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/delegate_with_actions.rs
  • dlp-api/src/instruction_builder/finalize.rs
  • dlp-api/src/instruction_builder/init_protocol_fees_vault.rs
  • dlp-api/src/instruction_builder/init_validator_fees_vault.rs
  • dlp-api/src/instruction_builder/mod.rs
  • dlp-api/src/instruction_builder/protocol_claim_fees.rs
  • dlp-api/src/instruction_builder/top_up_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/types/encryptable_types.rs
  • dlp-api/src/instruction_builder/types/mod.rs
  • dlp-api/src/instruction_builder/undelegate.rs
  • dlp-api/src/instruction_builder/undelegate_confined_account.rs
  • dlp-api/src/instruction_builder/validator_claim_fees.rs
  • dlp-api/src/instruction_builder/whitelist_validator_for_program.rs
  • dlp-api/src/lib.rs
  • src/args/delegate_with_actions.rs
  • src/args/mod.rs
  • src/compact/account_meta.rs
  • src/compact/instruction.rs
  • src/compact/mod.rs
  • src/consts.rs
  • src/diff/algorithm.rs
  • src/discriminator.rs
  • src/error.rs
  • src/lib.rs
  • src/processor/delegate_ephemeral_balance.rs
  • src/processor/fast/commit_state.rs
  • src/processor/fast/delegate.rs
  • src/processor/fast/delegate_with_actions.rs
  • src/processor/fast/finalize.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/undelegate.rs
  • src/processor/fast/undelegate_confined_account.rs
  • src/processor/fast/utils/mod.rs
  • src/requires.rs
  • src/state/utils/try_from_bytes.rs
  • tests/integration/programs/test-delegation/Cargo.toml
  • tests/test_call_handler.rs
  • tests/test_call_handler_v2.rs
  • tests/test_cleartext_with_insertable_encrypted.rs
  • tests/test_close_validator_fees_vault.rs
  • tests/test_commit_fees_on_undelegation.rs
  • tests/test_commit_finalize.rs
  • tests/test_commit_finalize_from_buffer.rs
  • tests/test_commit_on_curve.rs
  • tests/test_commit_state.rs
  • tests/test_commit_state_from_buffer.rs
  • tests/test_commit_state_with_program_config.rs
  • tests/test_commit_undelegate_zero_lamports_system_owned.rs
  • tests/test_delegate_on_curve.rs
  • tests/test_delegate_with_actions.rs
  • tests/test_delegation_confined_accounts.rs
  • tests/test_finalize.rs
  • tests/test_init_fees_vault.rs
  • tests/test_init_validator_fees_vault.rs
  • tests/test_lamports_settlement.rs
  • tests/test_protocol_claim_fees.rs
  • tests/test_top_up.rs
  • tests/test_undelegate.rs
  • tests/test_undelegate_confined_account.rs
  • tests/test_undelegate_on_curve.rs
  • tests/test_undelegate_without_commit.rs
  • tests/test_validator_claim_fees.rs
  • tests/test_whitelist_validator_for_program.rs
💤 Files with no reviewable changes (1)
  • src/processor/fast/utils/mod.rs

@snawaz snawaz force-pushed the snawaz/delegate-with-actions branch from cb2b798 to 682cd74 Compare March 11, 2026 18:44
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_commit_finalize.rs (1)

47-74: ⚠️ Potential issue | 🟠 Major

Use a distinct fee payer so this actually validates the new builder.

Both call sites still use authority as the transaction payer, so the runtime implicitly makes that account writable. That means these tests can pass even if dlp_api::instruction_builder::commit_finalize still emits the validator meta as readonly, which is exactly the bug this migration should catch. Use the payer returned by setup_program_test_env as the fee payer and keep authority only as the validator signer.

Suggested test change
-    let (banks, _, authority, blockhash) =
+    let (banks, payer, authority, blockhash) =
         setup_program_test_env(old_state.clone()).await;
@@
     let tx = Transaction::new_signed_with_payer(
         &[ix],
-        Some(&authority.pubkey()),
-        &[&authority],
+        Some(&payer.pubkey()),
+        &[&payer, &authority],
         blockhash,
     );
-    let (banks, _, authority, blockhash) = setup_program_test_env(vec![]).await;
+    let (banks, payer, authority, blockhash) =
+        setup_program_test_env(vec![]).await;
@@
     let tx = Transaction::new_signed_with_payer(
         &[ix],
-        Some(&authority.pubkey()),
-        &[&authority],
+        Some(&payer.pubkey()),
+        &[&payer, &authority],
         blockhash,
     );

Based on learnings: In src/instruction_builder/commit_finalize.rs and src/instruction_builder/commit_finalize_from_buffer.rs, the validator account (account index 0) must be marked writable because commit_finalize_internal may transfer from validator to delegated account, and using the validator as fee payer can mask that bug.

Also applies to: 119-143

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_commit_finalize.rs` around lines 47 - 74, The test uses authority
as the transaction fee payer which unintentionally makes the validator account
writable and hides a bug in dlp_api::instruction_builder::commit_finalize;
change the Transaction::new_signed_with_payer call to use the distinct payer
returned by setup_program_test_env as the fee payer (and include that payer
Keypair in the signers) while keeping authority only as the validator signer,
and make the equivalent change for the other call site (lines referenced around
119-143) so the builder must mark the validator account writable itself.
♻️ Duplicate comments (2)
src/processor/fast/delegate_with_actions.rs (2)

281-284: ⚠️ Potential issue | 🟠 Major

Add length check before copy_from_slice to prevent panic.

copy_from_slice will panic if delegate_buffer_account and delegated_account have different data lengths. Unlike finalize.rs which calls resize() before copying, this code only checks is_data_empty().

🛡️ Proposed fix
     // Copy the data from the buffer into the original account
     if !delegate_buffer_account.is_data_empty() {
+        if delegate_buffer_account.data_len() != delegated_account.data_len() {
+            return Err(DlpError::InvalidDataLength.into());
+        }
         let mut delegated_data = delegated_account.try_borrow_mut()?;
         let delegate_buffer_data = delegate_buffer_account.try_borrow()?;
         (*delegated_data).copy_from_slice(&delegate_buffer_data);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 281 - 284, The
current block uses delegated_account.try_borrow_mut() and
delegate_buffer_account.try_borrow() then calls copy_from_slice, but only checks
delegate_buffer_account.is_data_empty() so copy_from_slice can panic if lengths
differ; before calling copy_from_slice in the function handling
delegate_buffer_account and delegated_account, validate that
delegated_account.data_len() == delegate_buffer_account.data_len() (or call the
same resize() used in finalize.rs to make lengths equal) and return an error if
sizes mismatch, ensuring copy_from_slice is only invoked when the buffers are
the same length.

141-211: 🧹 Nitpick | 🔵 Trivial

Seed validation is duplicated from delegate.rs.

This ~70-line match block is nearly identical to delegate.rs (lines 157-212). Consider extracting to a shared helper function. Additionally, seeds.len() == 0 falls through to the _ arm returning TooManySeeds, which is misleading—a dedicated 0 => arm with a clearer error would improve clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 141 - 211, Extract
the repeated seed-length match into a shared helper (e.g. fn
build_seed_slices(seeds: &[Vec<u8>]) -> Result<Vec<&[u8]>, DlpError> or a
higher-level fn validate_delegate_pda(seeds: &[Vec<u8>], owner_program:
&Address, delegated_account: &Address) -> Result<(), ProgramError>) and call it
from both delegate_with_actions.rs and delegate.rs to remove duplication; inside
that helper construct the &[&[u8]] slice for Address::find_program_address,
handle the zero-length case explicitly (add a 0 => return
Err(DlpError::NoSeeds.into()) or a clearly named error instead of falling
through to TooManySeeds), preserve returning ProgramError::InvalidSeeds when
derived_pda != delegated_account, and keep existing references to
Address::find_program_address, args.delegate.seeds, DlpError::TooManySeeds, and
ProgramError::InvalidSeeds to locate the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Line 57: The Cargo.toml has a dependency mismatch: solana-instruction =
"3.0.0" conflicts with the current solana-program version range (which pins its
own dependencies to <3.0.0). Fix by either (A) bumping the solana-program
version requirement to accept 3.x (e.g., change the solana-program constraint to
">=1.16, <4.0.0") so solana-instruction 3.0.0 and solana-pubkey ^3.0.0 resolve,
or (B) downgrade solana-instruction to a 2.x release compatible with the
existing solana-program range (e.g., set solana-instruction to a 2.x version
that matches solana-program), updating the Cargo.toml dependency entry
accordingly.

In `@src/lib.rs`:
- Line 27: The compact module was made public but relies on a hidden invariant
that program_id values fit in 6 bits (0..=63); update either the compact
encoding path to validate/enforce this bound or document it in public rustdoc.
Specifically, add explicit validation in the
compact::instruction::from_instruction path (and any public builders in the
compact module) to check the caller-supplied index_of result and the program_id
field are <= 63 and return a clear error if not, or add a visible rustdoc
comment on pub mod compact and the public builder functions that documents the
6-bit requirement for program_id/index_of so downstream users see the
constraint.
- Around line 43-44: The entrypoint/processor feature contract is broken:
entrypoint.rs calls fast_process_instruction and slow_process_instruction
unconditionally but those functions are cfg-gated with #[cfg(feature =
"processor")], causing build failures if entrypoint is enabled without
processor; fix by either making the Cargo.toml entrypoint feature depend on
processor (add "processor" to entrypoint = [...]), or gate the entrypoint module
with #[cfg(feature = "processor")] (or add conditional calls inside the
entrypoint code to only call fast_process_instruction/slow_process_instruction
when cfg(feature = "processor") is set) so the symbols and feature flags stay
consistent.

In `@src/processor/fast/delegate_with_actions.rs`:
- Around line 101-102: The current computation casts lengths to u8 then adds
them, which can overflow; change to compute signers_count and keys_count using
usize (e.g., let signers_count = args.actions.signers.len(); let keys_count =
signers_count + args.actions.non_signers.len();), then check keys_count against
a MAX_PUBKEYS constant (or a defined limit) and return/error if it exceeds the
max, and only then convert to u8 using try_from or checked cast; update
references to signers_count and keys_count where they expect u8 to use the
validated converted value to avoid overflow and enforce the MAX_PUBKEYS
constraint.

---

Outside diff comments:
In `@tests/test_commit_finalize.rs`:
- Around line 47-74: The test uses authority as the transaction fee payer which
unintentionally makes the validator account writable and hides a bug in
dlp_api::instruction_builder::commit_finalize; change the
Transaction::new_signed_with_payer call to use the distinct payer returned by
setup_program_test_env as the fee payer (and include that payer Keypair in the
signers) while keeping authority only as the validator signer, and make the
equivalent change for the other call site (lines referenced around 119-143) so
the builder must mark the validator account writable itself.

---

Duplicate comments:
In `@src/processor/fast/delegate_with_actions.rs`:
- Around line 281-284: The current block uses delegated_account.try_borrow_mut()
and delegate_buffer_account.try_borrow() then calls copy_from_slice, but only
checks delegate_buffer_account.is_data_empty() so copy_from_slice can panic if
lengths differ; before calling copy_from_slice in the function handling
delegate_buffer_account and delegated_account, validate that
delegated_account.data_len() == delegate_buffer_account.data_len() (or call the
same resize() used in finalize.rs to make lengths equal) and return an error if
sizes mismatch, ensuring copy_from_slice is only invoked when the buffers are
the same length.
- Around line 141-211: Extract the repeated seed-length match into a shared
helper (e.g. fn build_seed_slices(seeds: &[Vec<u8>]) -> Result<Vec<&[u8]>,
DlpError> or a higher-level fn validate_delegate_pda(seeds: &[Vec<u8>],
owner_program: &Address, delegated_account: &Address) -> Result<(),
ProgramError>) and call it from both delegate_with_actions.rs and delegate.rs to
remove duplication; inside that helper construct the &[&[u8]] slice for
Address::find_program_address, handle the zero-length case explicitly (add a 0
=> return Err(DlpError::NoSeeds.into()) or a clearly named error instead of
falling through to TooManySeeds), preserve returning ProgramError::InvalidSeeds
when derived_pda != delegated_account, and keep existing references to
Address::find_program_address, args.delegate.seeds, DlpError::TooManySeeds, and
ProgramError::InvalidSeeds to locate the code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4d3734ba-191d-42db-80dc-95e35c866db9

📥 Commits

Reviewing files that changed from the base of the PR and between cb2b798 and 682cd74.

⛔ Files ignored due to path filters (1)
  • tests/integration/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • Cargo.toml
  • src/consts.rs
  • src/error.rs
  • src/lib.rs
  • src/processor/fast/commit_state.rs
  • src/processor/fast/delegate.rs
  • src/processor/fast/delegate_with_actions.rs
  • src/processor/fast/finalize.rs
  • src/processor/fast/undelegate.rs
  • src/processor/fast/undelegate_confined_account.rs
  • src/processor/fast/utils/mod.rs
  • src/requires.rs
  • tests/integration/programs/test-delegation/Cargo.toml
  • tests/test_commit_finalize.rs
💤 Files with no reviewable changes (1)
  • src/processor/fast/utils/mod.rs

@magicblock-labs magicblock-labs deleted a comment from coderabbitai bot Mar 11, 2026
@snawaz snawaz force-pushed the snawaz/delegate-with-actions branch from 682cd74 to e0022fa Compare March 11, 2026 21:43
@snawaz snawaz requested a review from GabrielePicco March 11, 2026 21:48
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs (1)

72-90: ⚠️ Potential issue | 🟡 Minor

Documentation mismatch and erroneous account in size budget.

  1. Line 73 references "commit_diff_from_buffer instruction" but the function is for commit_finalize_from_buffer.

  2. Line 88 includes program_config_pda in the size budget calculation, but this account is not part of the instruction's account list (lines 49-56). The processor implementation (src/processor/fast/commit_finalize_from_buffer.rs:50-57) expects exactly 7 accounts and does not reference program_config_pda. Remove this account from the budget.

📝 Suggested fix
 ///
-/// Returns accounts-data-size budget for commit_diff_from_buffer instruction.
+/// Returns accounts-data-size budget for commit_finalize_from_buffer instruction.
 ///
 /// This value can be used with ComputeBudgetInstruction::SetLoadedAccountsDataSizeLimit
 ///
 pub fn commit_finalize_from_buffer_size_budget(
     delegated_account: AccountSizeClass,
 ) -> u32 {
     total_size_budget(&[
         DLP_PROGRAM_DATA_SIZE_CLASS,
         AccountSizeClass::Tiny, // validator
         delegated_account,      // delegated_account
         AccountSizeClass::Tiny, // delegation_record_pda
         AccountSizeClass::Tiny, // delegation_metadata_pda
         delegated_account,      // data_buffer
         AccountSizeClass::Tiny, // validator_fees_vault_pda
-        AccountSizeClass::Tiny, // program_config_pda
         AccountSizeClass::Tiny, // system_program
     ])
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs` around lines
72 - 90, The docstring and account list in
commit_finalize_from_buffer_size_budget are inconsistent with the instruction
and processor: update the doc comment to reference commit_finalize_from_buffer
(instead of commit_diff_from_buffer) and remove the Program Config PDA entry
from the total_size_budget array so the budget matches the 7 accounts expected
by the processor; adjust the array passed to total_size_budget in
commit_finalize_from_buffer_size_budget (remove the AccountSizeClass::Tiny
corresponding to program_config_pda) so it aligns with the accounts used by the
processor implementation.
dlp-api/src/instruction_builder/commit_diff.rs (1)

42-53: ⚠️ Potential issue | 🔴 Critical

Mark validator account writable in commit_diff instruction builder.

Line 45 marks validator as readonly, but process_commit_state_internal (invoked via process_commit_diff) performs a system::Transfer from the validator account when commit_record_lamports > delegation_record.lamports. Transfer source accounts must be writable; otherwise the instruction will fail at runtime.

Fix
-            AccountMeta::new_readonly(validator, true),
+            AccountMeta::new(validator, true),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_diff.rs` around lines 42 - 53, The
validator AccountMeta is currently created as read-only in the Instruction
accounts list which breaks runtime transfers; change the AccountMeta for the
validator in the Instruction builder from AccountMeta::new_readonly(validator,
true) to a writable meta (AccountMeta::new(validator, true)) so the
system::Transfer in process_commit_state_internal (called by
process_commit_diff) can debit the validator as the transfer source; update the
accounts vector where Instruction is constructed to mark validator writable.
src/requires.rs (1)

803-866: 🧹 Nitpick | 🔵 Trivial

Redundant #[cfg(feature = "processor")] on inner block.

The function require_authorization is already gated with #[cfg(feature = "processor")] on line 803. The inner #[cfg(all(not(feature = "unit_test_config"), feature = "processor"))] on line 822 can be simplified to #[cfg(not(feature = "unit_test_config"))] since the processor feature is already required by the outer gate.

♻️ Suggested simplification
-    #[cfg(all(not(feature = "unit_test_config"), feature = "processor"))]
+    #[cfg(not(feature = "unit_test_config"))]
     {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/requires.rs` around lines 803 - 866, The inner cfg attribute on the
second block inside require_authorization is redundant; change
#[cfg(all(not(feature = "unit_test_config"), feature = "processor"))] to
#[cfg(not(feature = "unit_test_config"))] so the code relies on the outer
#[cfg(feature = "processor")] on the require_authorization function and removes
the duplicate feature check while keeping the unit-test conditional intact.
tests/test_commit_finalize.rs (1)

52-74: 🧹 Nitpick | 🔵 Trivial

These tests still won’t catch a readonly validator meta in the new builder.

The happy-path helper only exercises the lamport-decrease branch, and the out-of-order case exits before any validator transfer. Combined with authority also being the fee payer, a regression that marks account 0 readonly in dlp_api::instruction_builder::commit_finalize would still pass here. Please assert ix.accounts[0].is_writable directly, or add an increase-path case with a distinct fee payer.

Based on learnings, commit_finalize_internal transfers extra lamports from the validator wallet on the increase path, so the validator account must be explicitly writable and using the validator as fee payer can hide a readonly metadata regression.

Also applies to: 124-143

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_commit_finalize.rs` around lines 52 - 74, The test currently only
exercises the lamport-decrease path and can miss a regression that marks the
validator meta readonly in dlp_api::instruction_builder::commit_finalize; update
the test in tests/test_commit_finalize.rs to assert that
ix.accounts[0].is_writable after building the instruction (i.e., check
ix.accounts[0].is_writable directly) and/or add a new increase-path case where
lamports increase (use CommitFinalizeArgs with a larger lamports and
data_is_diff as appropriate) using a distinct fee payer so the validator account
is not the fee payer; ensure the new case reaches
dlp_api::instruction_builder::commit_finalize -> commit_finalize_internal code
path that transfers lamports from the validator wallet so the validator meta
must be writable.
tests/test_finalize.rs (1)

55-64: 🧹 Nitpick | 🔵 Trivial

Assert the validator meta is writable.

Because authority is also the fee payer, this test still passes if dlp_api::instruction_builder::finalize regresses to AccountMeta::new_readonly(...). Add a direct check on ix.accounts[0].is_writable before submitting the transaction so the new public builder path is actually covered.

Minimal assertion
     let ix = dlp_api::instruction_builder::finalize(
         authority.pubkey(),
         DELEGATED_PDA_ID,
     );
+    assert!(ix.accounts[0].is_writable);

Based on learnings: the finalize builders must mark validator account 0 writable; fee-payer writability can otherwise mask regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_finalize.rs` around lines 55 - 64, Add an assertion that the first
AccountMeta returned by dlp_api::instruction_builder::finalize is writable to
ensure the builder marks the validator (account 0) as writable; specifically,
after creating ix (the Instruction), check ix.accounts[0].is_writable and assert
it is true before constructing the Transaction (this prevents the fee-payer
being writable from masking regressions in finalize's AccountMeta creation).
♻️ Duplicate comments (13)
.github/workflows/run-tests.yml (1)

54-54: ⚠️ Potential issue | 🟡 Minor

Pin the formatter nightly to a dated toolchain.

Line 54 still uses floating nightly for both rustup and cargo, so rustfmt output can change as the channel advances and start failing unrelated PRs. Use the same dated nightly identifier in both places.

Example fix
-        run: rustup component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt && cargo +nightly fmt -- --check
+        run: rustup component add --toolchain nightly-YYYY-MM-DD-x86_64-unknown-linux-gnu rustfmt && cargo +nightly-YYYY-MM-DD fmt -- --check
How do you pin a Rust nightly toolchain date for both `rustup component add --toolchain ...` and `cargo +toolchain ...` in GitHub Actions?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/run-tests.yml at line 54, Replace the floating "nightly"
toolchain with a pinned dated nightly identifier and use that same identifier in
both commands: change the rustup call "rustup component add --toolchain
nightly-x86_64-unknown-linux-gnu rustfmt" and the cargo invocation "cargo
+nightly fmt -- --check" to reference a single pinned toolchain string (e.g.
"nightly-YYYY-MM-DD-x86_64-unknown-linux-gnu") or a variable like RUST_TOOLCHAIN
and use it in both places so both rustup and cargo use the identical dated
nightly.
dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs (2)

49-51: ⚠️ Potential issue | 🟠 Major

Validator account should be writable for commit_finalize paths.

The validator account is marked new_readonly, but for commit_finalize operations where a lamport increase path may invoke a transfer from validator to delegated_account, the validator must be explicitly writable.

🔧 Proposed fix
         accounts: vec![
-            AccountMeta::new_readonly(validator, true),
+            AccountMeta::new(validator, true),
             AccountMeta::new(delegated_account, false),

Based on learnings: "In Solana programs, when a path may invoke a transfer (e.g., from args.validator to args.delegated_account), ensure the validator account (index 0) is explicitly marked writable using AccountMeta::new... Apply this pattern to files under src/instruction_builder where account metas are constructed."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs` around lines
49 - 51, The validator AccountMeta is incorrectly created with
AccountMeta::new_readonly in commit_finalize_from_buffer.rs but must be writable
for commit_finalize paths that may transfer lamports; replace the
AccountMeta::new_readonly(validator, true) usage with a writable AccountMeta
(use AccountMeta::new(validator, true) or equivalent) so the validator (index 0)
is writable, leaving AccountMeta::new(delegated_account, false) as-is; ensure
this change is applied where the accounts Vec is constructed for the
commit_finalize_from_buffer instruction.

16-17: ⚠️ Potential issue | 🟡 Minor

Documentation reference mismatch.

The doc comment references process_commit_diff_from_buffer but this function builds a commit_finalize_from_buffer instruction (discriminator is CommitFinalizeFromBuffer on line 59).

📝 Suggested fix
 /// Builds a commit state from buffer instruction.
-/// See [dlp::processor::process_commit_diff_from_buffer] for docs.
+/// See [dlp::processor::process_commit_finalize_from_buffer] for docs.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs` around lines
16 - 17, The doc comment incorrectly references
dlp::processor::process_commit_diff_from_buffer for the builder that creates a
CommitFinalizeFromBuffer instruction; update the documentation comment in
commit_finalize_from_buffer.rs to reference the correct processor function
(e.g., dlp::processor::process_commit_finalize_from_buffer) and/or rename the
referenced symbol to match the instruction discriminator
CommitFinalizeFromBuffer so the doc link and the builder
(CommitFinalizeFromBuffer) are consistent.
Makefile (1)

7-8: 🧹 Nitpick | 🔵 Trivial

Consider adding --all-targets to lint test code.

The current lint target excludes test modules, examples, and benchmarks. Adding --all-targets ensures clippy also analyzes #[cfg(test)] code and test files.

Additionally, the unit_test_config feature used by the test target isn't included, so any code gated by that feature won't be linted.

♻️ Proposed fix
 lint:
-	cargo clippy --features sdk,program -- -D warnings
+	cargo clippy --all-targets --features sdk,program,unit_test_config -- -D warnings
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 7 - 8, The lint target currently runs `cargo clippy
--features sdk,program -- -D warnings` which skips test/examples/bench targets
and misses code gated by `unit_test_config`; update the `lint` recipe so `cargo
clippy` runs with `--all-targets` and includes the `unit_test_config` feature
(i.e., add `--all-targets` and add `unit_test_config` to the `--features` list)
so `lint` analyzes `#[cfg(test)]` code and any test-gated code.
Cargo.toml (1)

56-57: ⚠️ Potential issue | 🟠 Major

Keep the Solana crate generation consistent.

solana-program = ">=1.16, <3.0.0" is still being mixed with solana-instruction = "3.0.0" and solana-pubkey = "3.0.0". That crosses Solana major generations and can break resolution or force duplicate core types into the build.

#!/bin/bash
set -euo pipefail

echo "=== solana-program 2.x core deps ==="
curl -s https://crates.io/api/v1/crates/solana-program/2.1.0/dependencies \
  | jq -r '.dependencies[] | select(.crate_id | test("^solana-(instruction|pubkey)$")) | "\(.crate_id) \(.req)"'

echo
echo "=== solana-instruction 3.0.0 core deps ==="
curl -s https://crates.io/api/v1/crates/solana-instruction/3.0.0/dependencies \
  | jq -r '.dependencies[] | select(.crate_id=="solana-pubkey") | "\(.crate_id) \(.req)"'

Also applies to: 85-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` around lines 56 - 57, The Cargo.toml mixes Solana major versions
(solana-program = ">=1.16, <3.0.0" while solana-instruction and solana-pubkey
are "3.0.0"), causing duplicate core types; update the Solana crate version pins
so all Solana crates use the same major version (e.g., align solana-program,
solana-instruction, and solana-pubkey to 3.x or all to 2.x) by changing the
version string for the mismatched crate(s) (look for the solana-program,
solana-instruction, and solana-pubkey entries) to a consistent major version
range.
dlp-api/src/encryption/mod.rs (1)

43-50: ⚠️ Potential issue | 🟠 Major

Keep invalid secret-key inputs recoverable.

Line 46 panics before this helper can return EncryptionError::InvalidEd25519SecretKey, so malformed caller input takes down the process even though the API is otherwise fallible. Return the error directly, or tighten the parameter to a fixed-size array if the helper is meant to accept only 64-byte secret keys.

🔧 Proposed change
 pub fn ed25519_secret_to_x25519(
     ed25519_secret_key: &[u8],
 ) -> Result<[u8; KEY_LEN], EncryptionError> {
-    assert_eq!(ed25519_secret_key.len(), 64);
+    if ed25519_secret_key.len() != 64 {
+        return Err(EncryptionError::InvalidEd25519SecretKey);
+    }
 
     init_sodium()?;
For libsodium-rs 0.2.0, what input size does `crypto_sign::SecretKey::from_bytes` require, and does Rust `assert_eq!` panic in release builds?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/encryption/mod.rs` around lines 43 - 50, The helper
ed25519_secret_to_x25519 currently panics on malformed input due to
assert_eq!(ed25519_secret_key.len(), 64);—remove that assertion and instead
validate the length and return EncryptionError::InvalidEd25519SecretKey when
ed25519_secret_key.len() != 64, or alternatively change the function signature
to accept a &[u8; 64] to enforce the size at compile time; ensure the call to
crypto_sign::SecretKey::from_bytes remains mapped to
EncryptionError::InvalidEd25519SecretKey on error and keep init_sodium()?
propagation intact.
dlp-api/src/instruction_builder/types/encryptable_types.rs (1)

89-100: ⚠️ Potential issue | 🟠 Major

Make to_compact fallible or keep it private.

This is a public conversion, but invalid index values still hit .expect(...). Since the 6-bit limit is an input-validation concern, library callers should get a normal error (or a non-public helper) instead of a panic on 64..=255.

Based on learnings, compact indices are only valid when the caller keeps them within the pubkey-table limit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/types/encryptable_types.rs` around lines 89 -
100, The public method EncryptableAccountMeta::to_compact currently panics on
out-of-range indices via .expect(...); make it safe by either (A) making
to_compact non-public (e.g., change pub fn to pub(crate) or remove pub) if it
should remain infallible internally, or (B) make it fallible by changing the
signature to return Result<dlp::compact::EncryptableAccountMeta, E> and
propagate the AccountMeta::try_new error instead of calling .expect (use
?/map_err to convert the error into your chosen E). Update callers of
EncryptableAccountMeta::to_compact accordingly if you choose the fallible
approach.
dlp-api/src/encrypt.rs (2)

174-195: ⚠️ Potential issue | 🟠 Major

Propagate crypto failures instead of panicking.

The expect(...) calls in this block bypass the Result<Self::Output, EncryptionError> contract. If account-meta, ix-data, or pubkey encryption fails, callers currently get a panic instead of Err.

🔧 Proposed change
-        let compact_instructions: Vec<MaybeEncryptedInstruction> = self
-            .into_iter()
-            .map(|ix| MaybeEncryptedInstruction {
-                program_id: index_of(&ix.program_id.pubkey),
-
-                accounts: ix
-                    .accounts
-                    .into_iter()
-                    .map(|meta| {
-                        let index = index_of(&meta.account_meta.pubkey);
-                        meta.to_compact(index)
-                            .encrypt(validator)
-                            .expect("account metadata encryption failed")
-                    })
-                    .collect(),
-
-                data: ix
-                    .data
-                    .encrypt(validator)
-                    .expect("instruction data encryption failed"),
-            })
-            .collect();
+        let compact_instructions: Vec<MaybeEncryptedInstruction> = self
+            .into_iter()
+            .map(|ix| -> Result<MaybeEncryptedInstruction, EncryptionError> {
+                Ok(MaybeEncryptedInstruction {
+                    program_id: index_of(&ix.program_id.pubkey),
+                    accounts: ix
+                        .accounts
+                        .into_iter()
+                        .map(|meta| {
+                            let index = index_of(&meta.account_meta.pubkey);
+                            meta.to_compact(index).encrypt(validator)
+                        })
+                        .collect::<Result<Vec<_>, _>>()?,
+                    data: ix.data.encrypt(validator)?,
+                })
+            })
+            .collect::<Result<Vec<_>, _>>()?;
...
-                non_signers: non_signers
-                    .into_iter()
-                    .map(|ns| {
-                        EncryptablePubkey {
-                            pubkey: ns.account_meta.pubkey,
-                            is_encryptable: ns.is_encryptable,
-                        }
-                        .encrypt(validator)
-                        .expect("pubkey encryption failed")
-                    })
-                    .collect(),
+                non_signers: non_signers
+                    .into_iter()
+                    .map(|ns| {
+                        EncryptablePubkey {
+                            pubkey: ns.account_meta.pubkey,
+                            is_encryptable: ns.is_encryptable,
+                        }
+                        .encrypt(validator)
+                    })
+                    .collect::<Result<Vec<_>, _>>()?,

Also applies to: 203-213

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/encrypt.rs` around lines 174 - 195, The code currently panics on
encryption failures by calling expect(...) inside the mapping that builds
MaybeEncryptedInstruction; change those expect calls to propagate errors by
using the ? operator (or map_err as needed) so the encrypt(...) calls for
account metadata and instruction data return Err(EncryptionError) up through the
function's Result<Self::Output, EncryptionError>; update the closure to return a
Result<MaybeEncryptedInstruction, EncryptionError> and collect into a
Result<Vec<MaybeEncryptedInstruction>, EncryptionError> (and similarly fix the
second occurrence that mirrors this block), ensuring the function signature and
any intermediate maps accommodate the propagated Result rather than unwrapping.

153-160: ⚠️ Potential issue | 🟠 Major

Return an error instead of panicking on pubkey-table overflow.

This method already returns Result, but exceeding MAX_PUBKEYS still aborts the caller with panic!. Oversized action sets should fail as normal validation so callers can split or reject them.

🔧 Proposed change
         if signers.len() + non_signers.len()
             > dlp::compact::MAX_PUBKEYS as usize
         {
-            panic!(
-                "delegate_with_actions supports at most {} unique pubkeys",
-                dlp::compact::MAX_PUBKEYS
-            );
+            return Err(EncryptionError::TooManyPubkeys {
+                max: dlp::compact::MAX_PUBKEYS,
+                actual: signers.len() + non_signers.len(),
+            });
         }

Based on learnings, the compact encoding only stays valid when the caller enforces the pubkey-table limit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/encrypt.rs` around lines 153 - 160, The code panics when
signers.len() + non_signers.len() exceeds dlp::compact::MAX_PUBKEYS; change this
to return a proper error from the function (e.g., a validation/overflow error)
instead of calling panic!. Locate the check around signers/non_signers in
delegate_with_actions (and any helper functions in encrypt.rs) and replace the
panic! block with an early return like Err(...) using the function's Result
error type (create or reuse a ValidationError/TooManyPubkeys variant or an
anyhow/error type if appropriate), ensuring the error message includes the
MAX_PUBKEYS value and context so callers can handle/split oversized action sets.
src/compact/instruction.rs (1)

6-9: ⚠️ Potential issue | 🟠 Major

program_id should not bypass the compact index invariant.

from_instruction() only produces table indices via index_of, but pub program_id: u8 lets downstream code construct or deserialize values outside the 0..63 range. That makes the public type able to represent states the compact encoding path assumes cannot exist. Make the field private and validate the bound at construction/deserialization boundaries.

Based on learnings: the compact program_id is expected to stay in the 0..63 range via the caller-provided index_of contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/instruction.rs` around lines 6 - 9, The Instruction struct
exposes program_id as pub which can violate the compact-index invariant (must be
0..63); make program_id private on Instruction, add a safe constructor/new or a
checked setter and a public accessor (e.g., Instruction::program_id()) and
enforce validation at creation/deserialization points (notably in
from_instruction() and any Deserialize/try_from implementations) by checking
index_of results are within 0..63 (inclusive of 0, exclusive of 64) and
returning an error when out of range so external code cannot construct invalid
Instruction instances.
tests/test_call_handler.rs (1)

256-261: ⚠️ Potential issue | 🟠 Major

Choose either the native processor or the BPF path.

With prefer_bpf(true), this setup does not guarantee processor!(dlp::slow_process_instruction) is the code under test. Either remove prefer_bpf(true) to exercise the native slow processor, or remove the processor hook if these tests are supposed to run against the compiled dlp artifact.

In solana_program_test, if ProgramTest::new is passed Some(processor!(my_processor)) and prefer_bpf(true) is also set, does ProgramTest execute the supplied native processor or the compiled BPF/SBF artifact when one is available?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_call_handler.rs` around lines 256 - 261, The test currently passes
a native processor via ProgramTest::new
(processor!(dlp::slow_process_instruction)) while also calling prefer_bpf(true),
which means the harness may run the compiled BPF/SBF artifact instead of the
supplied native handler; decide which path you intend and fix accordingly: if
you want to exercise the native slow processor, remove the prefer_bpf(true) call
so ProgramTest will use processor!(dlp::slow_process_instruction); if you want
to run the compiled dlp artifact, remove the
processor!(dlp::slow_process_instruction) argument (and keep prefer_bpf(true))
so ProgramTest uses dlp::ID’s compiled BPF/SBF program.
src/processor/fast/delegate_with_actions.rs (1)

158-207: 🧹 Nitpick | 🔵 Trivial

Zero seeds falls through to TooManySeeds, which is misleading.

When args.delegate.seeds.len() == 0, the match hits the _ arm and returns DlpError::TooManySeeds. This error message is confusing for the zero-seeds case. Consider adding an explicit 0 => arm with a more descriptive error (e.g., DlpError::NoSeeds or DlpError::InvalidSeeds).

💡 Suggested fix
         let seeds_to_validate: &[&[u8]] = match args.delegate.seeds.len() {
+            0 => return Err(DlpError::InvalidSeeds.into()),
             1 => &[&args.delegate.seeds[0]],
             // ... rest unchanged
             _ => return Err(DlpError::TooManySeeds.into()),
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 158 - 207, The
match for building seeds_to_validate treats len==0 as the default case and
returns DlpError::TooManySeeds, which is misleading; add an explicit 0 => arm
that returns a clear error (e.g., return Err(DlpError::NoSeeds.into()) or
DlpError::InvalidSeeds) before the other arms so args.delegate.seeds length 0 is
handled correctly, keeping the existing arms for 1..=8 and retaining the _ =>
Err(DlpError::TooManySeeds.into()) fallback.
src/compact/mod.rs (1)

296-300: ⚠️ Potential issue | 🟡 Minor

unwrap() on position lookup can panic if pubkey not found.

Line 298 uses .unwrap() which will panic if the pubkey is not in non_signers. Unlike the ClearText implementation (line 115) which was updated to use .expect() with a descriptive message, this instance still uses bare .unwrap().

💡 Suggested fix for consistency
             (old_total
                 + signers.len()
-                + non_signers.iter().position(|ns| &ns.pubkey == pk).unwrap())
+                + non_signers
+                    .iter()
+                    .position(|ns| &ns.pubkey == pk)
+                    .expect("pubkey must exist in signers or non_signers"))
                 as u8
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/mod.rs` around lines 296 - 300, The code uses .unwrap() on the
position lookup non_signers.iter().position(|ns| &ns.pubkey == pk).unwrap(),
which can panic if the pubkey is missing; replace this .unwrap() with
.expect(...) and provide a descriptive message (e.g. "pubkey not found in
non_signers when computing compact index") so the calculation of the byte cast
((old_total + signers.len() + ... ) as u8) fails with a clear error instead of
panicking silently; mirror the ClearText change that used .expect() with an
informative message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dlp-api/src/cpi/delegate_with_actions.rs`:
- Around line 17-32: delegate_with_actions currently reconstructs action signers
with AccountMeta::new_readonly which drops any writability computed earlier;
change the builder so it preserves the is_writable bit produced by
PostDelegationInstruction::encrypt by making PostDelegationActions expose signer
metadata (pubkey + is_writable) or a Vec<AccountMeta> directly, then map those
to AccountMeta::new(pk.into(), true) when is_writable is true and to
AccountMeta::new_readonly(pk.into(), true) when false. Update
PostDelegationActions and the code that builds it (the encrypt path in
PostDelegationInstruction::encrypt) so the writability flag is stored and
available to delegate_with_actions instead of only storing Pubkey.

In `@dlp-api/src/decrypt.rs`:
- Around line 16-30: The pubkey-table rebuild currently ignores inserted_signers
and inserted_non_signers which breaks PostDelegationActions index resolution;
update the API that performs pubkey table reconstruction (where DecryptError
variants like InvalidProgramIdIndex and InvalidAccountIndex are used) to either
accept the inserted pubkeys (pass inserted_signers and inserted_non_signers into
the function and prepend them to the pubkey vector before any index lookups) or
add and return a new DecryptError variant (e.g., UnsupportedInsertedAccounts)
whenever inserted_signers or inserted_non_signers count is nonzero so callers
fail fast; ensure all call sites and the index resolution logic reference the
provided inserted pubkeys when building the pubkey table.

In `@dlp-api/src/instruction_builder/commit_state_from_buffer.rs`:
- Around line 43-46: The validator account in the Instruction builder is
incorrectly created with AccountMeta::new_readonly(validator, true) which leaves
it non-writable and will break any validator-funded PDA creation or lamport
movement when the validator is not the fee payer; update the account
construction in the Instruction (the vec of AccountMeta entries used when
building the Instruction for dlp::id()) to use AccountMeta::new(validator, true)
so the validator is explicitly marked writable and retains signer status.

In `@dlp-api/src/instruction_builder/commit_state.rs`:
- Around line 42-45: The Instruction in commit_state currently marks the
validator account as readonly via AccountMeta::new_readonly(validator, true),
which fails when the instruction creates/updates PDAs and the validator must be
written; replace that AccountMeta::new_readonly call with
AccountMeta::new(validator, true) so the validator (index 0) is explicitly
writable, keeping the signer flag true and preserving the existing Instruction {
program_id: dlp::id(), accounts: vec![ ... ] } structure.

In `@dlp-api/src/instruction_builder/finalize.rs`:
- Around line 31-35: The Instruction constructed in finalize uses
AccountMeta::new_readonly for the validator but the subsequent close_pda calls
(around where close_pda transfers lamports to validator) modify the validator
account; change the validator AccountMeta in the Instruction to be writable (use
AccountMeta::new with signer flag as appropriate) so the validator is marked
writable when the program executes, ensuring the close_pda transfers can modify
it; update the AccountMeta construction near Instruction in finalize to
reference validator as writable.

---

Outside diff comments:
In `@dlp-api/src/instruction_builder/commit_diff.rs`:
- Around line 42-53: The validator AccountMeta is currently created as read-only
in the Instruction accounts list which breaks runtime transfers; change the
AccountMeta for the validator in the Instruction builder from
AccountMeta::new_readonly(validator, true) to a writable meta
(AccountMeta::new(validator, true)) so the system::Transfer in
process_commit_state_internal (called by process_commit_diff) can debit the
validator as the transfer source; update the accounts vector where Instruction
is constructed to mark validator writable.

In `@dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs`:
- Around line 72-90: The docstring and account list in
commit_finalize_from_buffer_size_budget are inconsistent with the instruction
and processor: update the doc comment to reference commit_finalize_from_buffer
(instead of commit_diff_from_buffer) and remove the Program Config PDA entry
from the total_size_budget array so the budget matches the 7 accounts expected
by the processor; adjust the array passed to total_size_budget in
commit_finalize_from_buffer_size_budget (remove the AccountSizeClass::Tiny
corresponding to program_config_pda) so it aligns with the accounts used by the
processor implementation.

In `@src/requires.rs`:
- Around line 803-866: The inner cfg attribute on the second block inside
require_authorization is redundant; change #[cfg(all(not(feature =
"unit_test_config"), feature = "processor"))] to #[cfg(not(feature =
"unit_test_config"))] so the code relies on the outer #[cfg(feature =
"processor")] on the require_authorization function and removes the duplicate
feature check while keeping the unit-test conditional intact.

In `@tests/test_commit_finalize.rs`:
- Around line 52-74: The test currently only exercises the lamport-decrease path
and can miss a regression that marks the validator meta readonly in
dlp_api::instruction_builder::commit_finalize; update the test in
tests/test_commit_finalize.rs to assert that ix.accounts[0].is_writable after
building the instruction (i.e., check ix.accounts[0].is_writable directly)
and/or add a new increase-path case where lamports increase (use
CommitFinalizeArgs with a larger lamports and data_is_diff as appropriate) using
a distinct fee payer so the validator account is not the fee payer; ensure the
new case reaches dlp_api::instruction_builder::commit_finalize ->
commit_finalize_internal code path that transfers lamports from the validator
wallet so the validator meta must be writable.

In `@tests/test_finalize.rs`:
- Around line 55-64: Add an assertion that the first AccountMeta returned by
dlp_api::instruction_builder::finalize is writable to ensure the builder marks
the validator (account 0) as writable; specifically, after creating ix (the
Instruction), check ix.accounts[0].is_writable and assert it is true before
constructing the Transaction (this prevents the fee-payer being writable from
masking regressions in finalize's AccountMeta creation).

---

Duplicate comments:
In @.github/workflows/run-tests.yml:
- Line 54: Replace the floating "nightly" toolchain with a pinned dated nightly
identifier and use that same identifier in both commands: change the rustup call
"rustup component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt" and
the cargo invocation "cargo +nightly fmt -- --check" to reference a single
pinned toolchain string (e.g. "nightly-YYYY-MM-DD-x86_64-unknown-linux-gnu") or
a variable like RUST_TOOLCHAIN and use it in both places so both rustup and
cargo use the identical dated nightly.

In `@Cargo.toml`:
- Around line 56-57: The Cargo.toml mixes Solana major versions (solana-program
= ">=1.16, <3.0.0" while solana-instruction and solana-pubkey are "3.0.0"),
causing duplicate core types; update the Solana crate version pins so all Solana
crates use the same major version (e.g., align solana-program,
solana-instruction, and solana-pubkey to 3.x or all to 2.x) by changing the
version string for the mismatched crate(s) (look for the solana-program,
solana-instruction, and solana-pubkey entries) to a consistent major version
range.

In `@dlp-api/src/encrypt.rs`:
- Around line 174-195: The code currently panics on encryption failures by
calling expect(...) inside the mapping that builds MaybeEncryptedInstruction;
change those expect calls to propagate errors by using the ? operator (or
map_err as needed) so the encrypt(...) calls for account metadata and
instruction data return Err(EncryptionError) up through the function's
Result<Self::Output, EncryptionError>; update the closure to return a
Result<MaybeEncryptedInstruction, EncryptionError> and collect into a
Result<Vec<MaybeEncryptedInstruction>, EncryptionError> (and similarly fix the
second occurrence that mirrors this block), ensuring the function signature and
any intermediate maps accommodate the propagated Result rather than unwrapping.
- Around line 153-160: The code panics when signers.len() + non_signers.len()
exceeds dlp::compact::MAX_PUBKEYS; change this to return a proper error from the
function (e.g., a validation/overflow error) instead of calling panic!. Locate
the check around signers/non_signers in delegate_with_actions (and any helper
functions in encrypt.rs) and replace the panic! block with an early return like
Err(...) using the function's Result error type (create or reuse a
ValidationError/TooManyPubkeys variant or an anyhow/error type if appropriate),
ensuring the error message includes the MAX_PUBKEYS value and context so callers
can handle/split oversized action sets.

In `@dlp-api/src/encryption/mod.rs`:
- Around line 43-50: The helper ed25519_secret_to_x25519 currently panics on
malformed input due to assert_eq!(ed25519_secret_key.len(), 64);—remove that
assertion and instead validate the length and return
EncryptionError::InvalidEd25519SecretKey when ed25519_secret_key.len() != 64, or
alternatively change the function signature to accept a &[u8; 64] to enforce the
size at compile time; ensure the call to crypto_sign::SecretKey::from_bytes
remains mapped to EncryptionError::InvalidEd25519SecretKey on error and keep
init_sodium()? propagation intact.

In `@dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs`:
- Around line 49-51: The validator AccountMeta is incorrectly created with
AccountMeta::new_readonly in commit_finalize_from_buffer.rs but must be writable
for commit_finalize paths that may transfer lamports; replace the
AccountMeta::new_readonly(validator, true) usage with a writable AccountMeta
(use AccountMeta::new(validator, true) or equivalent) so the validator (index 0)
is writable, leaving AccountMeta::new(delegated_account, false) as-is; ensure
this change is applied where the accounts Vec is constructed for the
commit_finalize_from_buffer instruction.
- Around line 16-17: The doc comment incorrectly references
dlp::processor::process_commit_diff_from_buffer for the builder that creates a
CommitFinalizeFromBuffer instruction; update the documentation comment in
commit_finalize_from_buffer.rs to reference the correct processor function
(e.g., dlp::processor::process_commit_finalize_from_buffer) and/or rename the
referenced symbol to match the instruction discriminator
CommitFinalizeFromBuffer so the doc link and the builder
(CommitFinalizeFromBuffer) are consistent.

In `@dlp-api/src/instruction_builder/types/encryptable_types.rs`:
- Around line 89-100: The public method EncryptableAccountMeta::to_compact
currently panics on out-of-range indices via .expect(...); make it safe by
either (A) making to_compact non-public (e.g., change pub fn to pub(crate) or
remove pub) if it should remain infallible internally, or (B) make it fallible
by changing the signature to return Result<dlp::compact::EncryptableAccountMeta,
E> and propagate the AccountMeta::try_new error instead of calling .expect (use
?/map_err to convert the error into your chosen E). Update callers of
EncryptableAccountMeta::to_compact accordingly if you choose the fallible
approach.

In `@Makefile`:
- Around line 7-8: The lint target currently runs `cargo clippy --features
sdk,program -- -D warnings` which skips test/examples/bench targets and misses
code gated by `unit_test_config`; update the `lint` recipe so `cargo clippy`
runs with `--all-targets` and includes the `unit_test_config` feature (i.e., add
`--all-targets` and add `unit_test_config` to the `--features` list) so `lint`
analyzes `#[cfg(test)]` code and any test-gated code.

In `@src/compact/instruction.rs`:
- Around line 6-9: The Instruction struct exposes program_id as pub which can
violate the compact-index invariant (must be 0..63); make program_id private on
Instruction, add a safe constructor/new or a checked setter and a public
accessor (e.g., Instruction::program_id()) and enforce validation at
creation/deserialization points (notably in from_instruction() and any
Deserialize/try_from implementations) by checking index_of results are within
0..63 (inclusive of 0, exclusive of 64) and returning an error when out of range
so external code cannot construct invalid Instruction instances.

In `@src/compact/mod.rs`:
- Around line 296-300: The code uses .unwrap() on the position lookup
non_signers.iter().position(|ns| &ns.pubkey == pk).unwrap(), which can panic if
the pubkey is missing; replace this .unwrap() with .expect(...) and provide a
descriptive message (e.g. "pubkey not found in non_signers when computing
compact index") so the calculation of the byte cast ((old_total + signers.len()
+ ... ) as u8) fails with a clear error instead of panicking silently; mirror
the ClearText change that used .expect() with an informative message.

In `@src/processor/fast/delegate_with_actions.rs`:
- Around line 158-207: The match for building seeds_to_validate treats len==0 as
the default case and returns DlpError::TooManySeeds, which is misleading; add an
explicit 0 => arm that returns a clear error (e.g., return
Err(DlpError::NoSeeds.into()) or DlpError::InvalidSeeds) before the other arms
so args.delegate.seeds length 0 is handled correctly, keeping the existing arms
for 1..=8 and retaining the _ => Err(DlpError::TooManySeeds.into()) fallback.

In `@tests/test_call_handler.rs`:
- Around line 256-261: The test currently passes a native processor via
ProgramTest::new (processor!(dlp::slow_process_instruction)) while also calling
prefer_bpf(true), which means the harness may run the compiled BPF/SBF artifact
instead of the supplied native handler; decide which path you intend and fix
accordingly: if you want to exercise the native slow processor, remove the
prefer_bpf(true) call so ProgramTest will use
processor!(dlp::slow_process_instruction); if you want to run the compiled dlp
artifact, remove the processor!(dlp::slow_process_instruction) argument (and
keep prefer_bpf(true)) so ProgramTest uses dlp::ID’s compiled BPF/SBF program.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 24e9a1e5-0c74-4d03-8158-e1de1f68d206

📥 Commits

Reviewing files that changed from the base of the PR and between 95ca88d and e0022fa.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • tests/integration/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (85)
  • .coderabbit.yaml
  • .github/workflows/run-tests.yml
  • Cargo.toml
  • Makefile
  • dlp-api/Cargo.toml
  • dlp-api/src/cpi/delegate_with_actions.rs
  • dlp-api/src/cpi/mod.rs
  • dlp-api/src/decrypt.rs
  • dlp-api/src/encrypt.rs
  • dlp-api/src/encryption/mod.rs
  • dlp-api/src/instruction_builder/call_handler.rs
  • dlp-api/src/instruction_builder/call_handler_v2.rs
  • dlp-api/src/instruction_builder/close_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/close_validator_fees_vault.rs
  • dlp-api/src/instruction_builder/commit_diff.rs
  • dlp-api/src/instruction_builder/commit_diff_from_buffer.rs
  • dlp-api/src/instruction_builder/commit_finalize.rs
  • dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs
  • dlp-api/src/instruction_builder/commit_state.rs
  • dlp-api/src/instruction_builder/commit_state_from_buffer.rs
  • dlp-api/src/instruction_builder/delegate.rs
  • dlp-api/src/instruction_builder/delegate_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/delegate_with_actions.rs
  • dlp-api/src/instruction_builder/finalize.rs
  • dlp-api/src/instruction_builder/init_protocol_fees_vault.rs
  • dlp-api/src/instruction_builder/init_validator_fees_vault.rs
  • dlp-api/src/instruction_builder/mod.rs
  • dlp-api/src/instruction_builder/protocol_claim_fees.rs
  • dlp-api/src/instruction_builder/top_up_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/types/encryptable_types.rs
  • dlp-api/src/instruction_builder/types/mod.rs
  • dlp-api/src/instruction_builder/undelegate.rs
  • dlp-api/src/instruction_builder/undelegate_confined_account.rs
  • dlp-api/src/instruction_builder/validator_claim_fees.rs
  • dlp-api/src/instruction_builder/whitelist_validator_for_program.rs
  • dlp-api/src/lib.rs
  • src/args/delegate_with_actions.rs
  • src/args/mod.rs
  • src/compact/account_meta.rs
  • src/compact/instruction.rs
  • src/compact/mod.rs
  • src/consts.rs
  • src/diff/algorithm.rs
  • src/discriminator.rs
  • src/error.rs
  • src/lib.rs
  • src/processor/delegate_ephemeral_balance.rs
  • src/processor/fast/commit_state.rs
  • src/processor/fast/delegate.rs
  • src/processor/fast/delegate_with_actions.rs
  • src/processor/fast/finalize.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/undelegate.rs
  • src/processor/fast/undelegate_confined_account.rs
  • src/processor/fast/utils/mod.rs
  • src/requires.rs
  • src/state/utils/try_from_bytes.rs
  • tests/integration/programs/test-delegation/Cargo.toml
  • tests/test_call_handler.rs
  • tests/test_call_handler_v2.rs
  • tests/test_cleartext_with_insertable_encrypted.rs
  • tests/test_close_validator_fees_vault.rs
  • tests/test_commit_fees_on_undelegation.rs
  • tests/test_commit_finalize.rs
  • tests/test_commit_finalize_from_buffer.rs
  • tests/test_commit_on_curve.rs
  • tests/test_commit_state.rs
  • tests/test_commit_state_from_buffer.rs
  • tests/test_commit_state_with_program_config.rs
  • tests/test_commit_undelegate_zero_lamports_system_owned.rs
  • tests/test_delegate_on_curve.rs
  • tests/test_delegate_with_actions.rs
  • tests/test_delegation_confined_accounts.rs
  • tests/test_finalize.rs
  • tests/test_init_fees_vault.rs
  • tests/test_init_validator_fees_vault.rs
  • tests/test_lamports_settlement.rs
  • tests/test_protocol_claim_fees.rs
  • tests/test_top_up.rs
  • tests/test_undelegate.rs
  • tests/test_undelegate_confined_account.rs
  • tests/test_undelegate_on_curve.rs
  • tests/test_undelegate_without_commit.rs
  • tests/test_validator_claim_fees.rs
  • tests/test_whitelist_validator_for_program.rs
💤 Files with no reviewable changes (1)
  • src/processor/fast/utils/mod.rs

@magicblock-labs magicblock-labs deleted a comment from coderabbitai bot Mar 11, 2026
@magicblock-labs magicblock-labs deleted a comment from coderabbitai bot Mar 11, 2026
Copy link
Contributor

@GabrielePicco GabrielePicco left a comment

Choose a reason for hiding this comment

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

LGTM!

@magicblock-labs magicblock-labs deleted a comment from coderabbitai bot Mar 12, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
dlp-api/src/instruction_builder/commit_finalize.rs (1)

55-61: ⚠️ Potential issue | 🔴 Critical

Mark every mutated account writable in commit_finalize.

process_commit_finalize does not only write delegated_account: it also updates delegation_record.lamports, debits validator on the lamport-increase path, and credits validator_fees_vault on the decrease path. Keeping those three metas readonly makes the builder emit instructions that fail as soon as the processor touches them.

Proposed fix
         Instruction {
             program_id: dlp::id(),
             accounts: vec![
-                AccountMeta::new_readonly(validator, true),
+                AccountMeta::new(validator, true),
                 AccountMeta::new(delegated_account, false),
-                AccountMeta::new_readonly(delegation_record.0, false),
+                AccountMeta::new(delegation_record.0, false),
                 AccountMeta::new(delegation_metadata.0, false),
-                AccountMeta::new_readonly(validator_fees_vault.0, false),
+                AccountMeta::new(validator_fees_vault.0, false),
                 AccountMeta::new_readonly(system_program::id(), false),
             ],

Based on learnings: the finalize path writes delegation_record.lamports, transfers from args.validator on increases, and credits validator_fees_vault on decreases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_finalize.rs` around lines 55 - 61, The
accounts list built in commit_finalize currently marks validator,
delegation_record, and validator_fees_vault as readonly but
process_commit_finalize mutates delegation_record.lamports and may transfer
lamports from args.validator or credit validator_fees_vault; update the
AccountMeta entries in the commit_finalize builder so that validator,
delegation_record.0, and validator_fees_vault.0 are created as writable (use
AccountMeta::new instead of AccountMeta::new_readonly) while keeping
delegation_metadata and system_program as readonly as appropriate.
tests/test_commit_finalize.rs (1)

52-68: ⚠️ Potential issue | 🟡 Minor

Assert the validator meta is writable before sending the transaction.

Both tests use authority as the fee payer, so they can still pass if the migrated dlp_api::instruction_builder::commit_finalize accidentally returns account 0 as readonly. That would break callers that use a different fee payer.

🔎 Suggested test hardening
     let (ix, pdas) = dlp_api::instruction_builder::commit_finalize(
         authority.pubkey(),
         DELEGATED_PDA_ID,
         &mut CommitFinalizeArgs {
             commit_id: 1,
             allow_undelegation: true.into(),
             data_is_diff: data_is_diff.into(),
             lamports: new_account_balance,
             bumps: Default::default(),
             reserved_padding: Default::default(),
         },
         &if data_is_diff {
             compute_diff(&old_state, &new_state).to_vec()
         } else {
             new_state.clone()
         },
     );
+    assert_eq!(ix.accounts[0].pubkey, authority.pubkey());
+    assert!(ix.accounts[0].is_writable);

Apply the same assertion in test_commit_finalize_out_of_order.

Based on learnings In the delegation-program (src/instruction_builder/commit_finalize.rs and src/instruction_builder/commit_finalize_from_buffer.rs), the validator account (account index 0) must be marked writable (AccountMeta::new) because the commit_finalize_internal logic may invoke pinocchio_system::instructions::Transfer from args.validator to args.delegated_account.

Also applies to: 124-136

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_commit_finalize.rs` around lines 52 - 68, Assert that the
validator account meta is writable after building the instruction: after the let
(ix, pdas) = dlp_api::instruction_builder::commit_finalize(...) call, add an
assertion like assert!(pdas[0].is_writable(), "validator meta must be
writable"); do the same in test_commit_finalize_out_of_order; this ensures the
validator (account index 0) returned by
commit_finalize/commit_finalize_from_buffer is AccountMeta::new (writable)
rather than readonly.
♻️ Duplicate comments (17)
Makefile (1)

7-8: 🧹 Nitpick | 🔵 Trivial

--all-targets still missing from lint target.

The addition of --features sdk,program addresses the feature-flag gap, but test modules, examples, and benchmarks remain unlinted without --all-targets. This means #[cfg(test)] blocks and test files (e.g., those using unit_test_config) won't be checked by clippy.

Suggested improvement
 lint:
-	cargo clippy --features sdk,program -- -D warnings
+	cargo clippy --all-targets --features sdk,program,unit_test_config -- -D warnings
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 7 - 8, The lint Makefile target currently runs `cargo
clippy --features sdk,program -- -D warnings` but omits test
modules/examples/benches; update the `lint` target (the Makefile `lint` rule) to
include `--all-targets` in the cargo clippy invocation so clippy also checks
#[cfg(test)] code, examples, benches, and tests while preserving the existing
`--features sdk,program` and `-D warnings` flags.
dlp-api/src/instruction_builder/commit_state_from_buffer.rs (1)

43-46: ⚠️ Potential issue | 🟠 Major

Mark the validator account writable here.

Line 46 still leaves the validator readonly. If this path creates PDAs or moves lamports from the validator, the instruction will fail whenever the validator is not also the writable funding account.

Proposed fix
     Instruction {
         program_id: dlp::id(),
         accounts: vec![
-            AccountMeta::new_readonly(validator, true),
+            AccountMeta::new(validator, true),
             AccountMeta::new_readonly(delegated_account, false),
             AccountMeta::new(commit_state_pda, false),
             AccountMeta::new(commit_record_pda, false),
#!/bin/bash
set -euo pipefail

echo "Inspect builder-side validator AccountMeta usage:"
rg -nC2 --type rust 'AccountMeta::new(_readonly)?\(validator,\s*true\)'

echo
echo "Inspect the on-chain CommitStateFromBuffer flow for validator-funded account creation / lamport movement:"
rg -nC6 --type rust 'fn\s+process_commit_state_from_buffer\b|create_account|transfer|invoke_signed\(|invoke\('

Based on learnings: In Solana programs, when a path may invoke a transfer (e.g., from args.validator to args.delegated_account), ensure the validator account (index 0) is explicitly marked writable using AccountMeta::new. Relying on the runtime's fee-payer writability is not a substitute for explicitness. Add this writable marker for correctness and clarity in all relevant instruction builders (e.g., commit_finalize_internal.rs, commit_finalize_from_buffer.rs). Apply this pattern to files under src/instruction_builder where account metas are constructed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_state_from_buffer.rs` around lines 43
- 46, The validator AccountMeta is currently created as readonly
(AccountMeta::new_readonly(validator, true)) which will fail when the on-chain
CommitStateFromBuffer flow needs to create PDAs or move lamports; change the
validator AccountMeta to be writable by using AccountMeta::new(validator, true)
in commit_state_from_buffer.rs (and apply the same pattern in other builders
like commit_finalize_internal.rs and commit_finalize_from_buffer.rs) so the
validator is explicitly writable when passed to the instruction builder.
src/processor/delegate_ephemeral_balance.rs (2)

91-102: ⚠️ Potential issue | 🟡 Minor

Validate the rederived PDA accounts before the self-CPI.

These PDAs are recomputed here, but the supplied delegate_buffer, delegation_record, and delegation_metadata accounts are never checked against them. A mismatched account list now fails later inside the inner invoke instead of returning a deterministic local validation error.

🛡️ Suggested guard
     let delegation_metadata_pda =
         delegation_metadata_pda_from_delegated_account(
             ephemeral_balance_account.key,
         );
+    if delegate_buffer.key != &delegate_buffer_pda
+        || delegation_record.key != &delegation_record_pda
+        || delegation_metadata.key != &delegation_metadata_pda
+    {
+        return Err(ProgramError::InvalidArgument);
+    }
     let mut data = DlpDiscriminator::Delegate.to_vec();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/delegate_ephemeral_balance.rs` around lines 91 - 102, Recompute
the expected PDAs using
delegate_buffer_pda_from_delegated_account_and_owner_program,
delegation_record_pda_from_delegated_account, and
delegation_metadata_pda_from_delegated_account and immediately validate that the
provided accounts (delegate_buffer, delegation_record, delegation_metadata) have
matching pubkeys; if any mismatch, return a deterministic program error (e.g.,
an InvalidAccount or custom error) before performing the self-CPI invoke to
avoid failing inside the inner invoke.

107-119: ⚠️ Potential issue | 🟡 Minor

Check writable privileges before calling invoke_signed.

The inner instruction marks payer, ephemeral_balance_account, delegate_buffer, delegation_record, and delegation_metadata as writable, but this processor never validates that the outer invocation granted those privileges. If any of them arrives readonly, the failure is deferred to the CPI with a generic privilege error instead of the usual local validation path.

Also applies to: 121-134

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/delegate_ephemeral_balance.rs` around lines 107 - 119, The
constructed inner Instruction marks payer, ephemeral_balance_account,
delegate_buffer_pda, delegation_record_pda, and delegation_metadata_pda as
writable but the processor does not validate those writable privileges before
calling invoke_signed; update the code in delegate_ephemeral_balance.rs to
explicitly check each corresponding AccountInfo (payer,
ephemeral_balance_account, delegate_buffer, delegation_record,
delegation_metadata) has is_writable == true and return a clear error (e.g.,
ProgramError::InvalidAccountData or a custom PrivilegeEscalation error) if any
are readonly, and apply the same checks for the second invoke_signed call region
(the block corresponding to lines 121-134) so CPI failures are caught locally
with a descriptive error instead of a generic privilege error.
dlp-api/src/instruction_builder/commit_state.rs (1)

42-45: ⚠️ Potential issue | 🟠 Major

This still leaves the validator readonly.

If process_commit_state uses validator as the payer for commit-side PDA creation/update, this meta has to be writable. Leaving index 0 readonly keeps the relayer/alternate-fee-payer failure mode intact.

Proposed fix
     Instruction {
         program_id: dlp::id(),
         accounts: vec![
-            AccountMeta::new_readonly(validator, true),
+            AccountMeta::new(validator, true),
             AccountMeta::new_readonly(delegated_account, false),
             AccountMeta::new(commit_state_pda, false),
             AccountMeta::new(commit_record_pda, false),

Based on learnings: in instruction builders, the validator account at index 0 should be explicitly writable instead of relying on fee-payer writability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_state.rs` around lines 42 - 45, The
validator AccountMeta in the Instruction construction inside commit_state.rs is
created as readonly (AccountMeta::new_readonly(validator, true)), which leaves
index 0 non-writable and breaks cases where process_commit_state uses the
validator as the payer; update the AccountMeta for the validator to be writable
(use the writable constructor for the validator account at index 0 so it is
explicitly mutable and still a signer) so the commit-side PDA creation/update
can write to it.
dlp-api/src/instruction_builder/finalize.rs (1)

33-35: ⚠️ Potential issue | 🟠 Major

Mark the validator writable in finalize.

The close_pda calls in the processor transfer lamports to the validator account, which requires it to be writable. This was flagged in a previous review and remains unaddressed.

Suggested fix
         accounts: vec![
-            AccountMeta::new_readonly(validator, true),
+            AccountMeta::new(validator, true),
             AccountMeta::new(delegated_account, false),

Based on learnings: "In Solana programs, when a path may invoke a transfer... ensure the validator account (index 0) is explicitly marked writable using AccountMeta::new."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/finalize.rs` around lines 33 - 35, In the
finalize instruction builder update the AccountMeta for the validator so it is
writable: replace or change the AccountMeta::new_readonly(validator, true) entry
used in accounts within finalize to use AccountMeta::new(validator, true)
(leaving the delegated_account AccountMeta::new(delegated_account, false) as-is)
so the validator account (referenced by validator) is marked writable for
subsequent close_pda transfers.
Cargo.toml (2)

76-77: ⚠️ Potential issue | 🟡 Minor

Align solana-sdk version ceiling and relax rand pin.

  1. solana-sdk has no upper bound but should match solana-program's <3.0.0 constraint
  2. rand = "=0.8.5" exact pin blocks security patch updates; use ^0.8.5 instead
Suggested fix
-solana-sdk = { version = ">=1.16", optional = true }
-rand = { version = "=0.8.5", features = ["small_rng"], optional = true }
+solana-sdk = { version = ">=1.16, <3.0.0", optional = true }
+rand = { version = "^0.8.5", features = ["small_rng"], optional = true }

Also update the dev-dependency on line 84:

-rand = { version = "=0.8.5", features = ["small_rng"] }
+rand = { version = "^0.8.5", features = ["small_rng"] }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` around lines 76 - 77, Update the Cargo.toml dependency lines to
constrain solana-sdk to the same upper bound as solana-program (e.g., "<3.0.0")
and relax the rand pin to allow patch updates by changing the exact version
"=0.8.5" to a caret requirement like "^0.8.5"; also apply the same rand change
to the corresponding dev-dependency mentioned on line 84 so all rand entries use
the caret form and solana-sdk aligns with solana-program's version range.

57-57: ⚠️ Potential issue | 🟠 Major

Version incompatibility between Solana crates.

solana-program is constrained to <3.0.0, but solana-instruction = "3.0.0" and solana-pubkey = "3.0.0" (dev-dep) are pinned to 3.x versions. These Solana crates are released together and must match versions. This will cause dependency resolution conflicts.

Either:

  1. Upgrade solana-program to allow 3.x: >=1.16, <4.0.0
  2. Downgrade solana-instruction and solana-pubkey to 2.x versions
Option 1: Upgrade solana-program
-solana-program = { version = ">=1.16, <3.0.0" }
+solana-program = { version = ">=1.16, <4.0.0" }
#!/bin/bash
# Verify version compatibility by checking solana-instruction 3.0.0 dependencies
echo "=== solana-instruction 3.0.0 dependencies ==="
curl -s "https://crates.io/api/v1/crates/solana-instruction/3.0.0/dependencies" | jq '.dependencies[] | select(.crate_id | contains("solana")) | {crate: .crate_id, req: .req}'

echo -e "\n=== Check if solana-program 2.x uses solana-instruction 2.x ==="
curl -s "https://crates.io/api/v1/crates/solana-program/2.1.0/dependencies" | jq '.dependencies[] | select(.crate_id == "solana-instruction") | {crate: .crate_id, req: .req}'

Also applies to: 85-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` at line 57, The Cargo.toml has mismatched Solana crate versions:
solana-program is constrained to <3.0.0 while solana-instruction (and dev-dep
solana-pubkey) are pinned to 3.0.0; fix by making all Solana crates use the same
major version—either update the solana-program version constraint to allow 3.x
(e.g. change its requirement to something like >=1.16, <4.0.0) so it aligns with
solana-instruction/solana-pubkey 3.0.0, or downgrade solana-instruction and
solana-pubkey to 2.x so they match the existing solana-program constraint;
update the corresponding version strings in Cargo.toml for the symbols
solana-program, solana-instruction, and solana-pubkey accordingly and run cargo
update to verify resolution.
dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs (2)

49-51: ⚠️ Potential issue | 🟠 Major

Validator account should be writable for commit_finalize paths.

The validator account is marked new_readonly, but for commit_finalize operations where a lamport increase path may invoke a transfer from validator to delegated_account, the validator should be explicitly writable.

Proposed fix
         accounts: vec![
-            AccountMeta::new_readonly(validator, true),
+            AccountMeta::new(validator, true),
             AccountMeta::new(delegated_account, false),

Based on learnings: "In Solana programs, when a path may invoke a transfer (e.g., from args.validator to args.delegated_account), ensure the validator account (index 0) is explicitly marked writable using AccountMeta::new."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs` around lines
49 - 51, The validator account is incorrectly marked readonly in the
commit_finalize account list; change the AccountMeta for validator from
AccountMeta::new_readonly(validator, true) to AccountMeta::new(validator, true)
so the validator (index 0) is explicitly writable for commit_finalize paths that
may transfer lamports to delegated_account.

16-17: ⚠️ Potential issue | 🟡 Minor

Documentation reference mismatch.

The doc comment references process_commit_diff_from_buffer but this function builds a commit_finalize_from_buffer instruction (as indicated by the discriminator on line 59).

Suggested fix
 /// Builds a commit state from buffer instruction.
-/// See [dlp::processor::process_commit_diff_from_buffer] for docs.
+/// See [dlp::processor::process_commit_finalize_from_buffer] for docs.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs` around lines
16 - 17, The doc comment incorrectly references process_commit_diff_from_buffer
for the builder that constructs a commit_finalize_from_buffer instruction;
update the documentation to reference the correct processing function (e.g.,
process_commit_finalize_from_buffer) or the instruction name
commit_finalize_from_buffer to match the discriminator used in this file (see
discriminator at line with commit_finalize_from_buffer) so the docs and code are
consistent.
src/compact/instruction.rs (1)

5-22: ⚠️ Potential issue | 🟠 Major

Enforce the 6-bit program_id invariant at the type boundary.

AccountMeta already rejects indices >= 64, but Instruction can still carry program_id >= 64: the field is a raw pub u8, Deserialize accepts any byte, and from_instruction stores index_of(...) unchecked. That leaves invalid compact instructions constructible if a caller or parser violates the contract.

💡 Minimal guard in from_instruction
     ) -> Instruction {
+        let program_id = index_of(ix.program_id, false);
+        assert!(program_id < 64, "compact program index must fit in 6 bits");
+
         Instruction {
-            program_id: index_of(ix.program_id, false),
+            program_id,

Based on learnings: In src/compact/instruction.rs, the program_id is stored as u8 but currently relies on the caller's index_of callback to guarantee it fits in 6 bits (0-63). Add explicit internal validation in from_instruction and guard downstream code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compact/instruction.rs` around lines 5 - 22, from_instruction currently
accepts any u8 from the index_of callback, so add a runtime check to ensure the
returned program_id is < 64 and surface failure instead of silently storing
invalid values: change Instruction::from_instruction to return
Result<Instruction, SomeError> (or panic with a clear message if you prefer),
validate the value of index_of(ix.program_id, false) < 64 before constructing
Instruction, and propagate/return an error when it violates the 6-bit invariant;
additionally, implement a custom Deserialize (or add a post-deserialize check)
for Instruction that rejects deserialized program_id >= 64 so the invariant is
enforced at the parsing boundary as well (references: Instruction,
from_instruction, program_id, Deserialize, AccountMeta).
tests/test_call_handler.rs (1)

256-261: ⚠️ Potential issue | 🟠 Major

prefer_bpf(true) bypasses the supplied native slow processor.

This setup will not exercise dlp::slow_process_instruction when a matching BPF artifact is available, so the test is no longer validating the slow path it claims to cover. Either remove prefer_bpf(true) to force the native processor, or drop the processor!(...) argument if the intent is to test the compiled program instead.

In solana-program-test 1.16, if ProgramTest::new is passed Some(processor!(my_processor)) and prefer_bpf(true) is also set, which processor runs when a matching BPF artifact is available?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_call_handler.rs` around lines 256 - 261, The test currently calls
ProgramTest::new(..., processor!(dlp::slow_process_instruction)) but then sets
prefer_bpf(true), which causes the BPF artifact to be chosen over the supplied
native processor; update the test so it actually exercises
dlp::slow_process_instruction by either removing the prefer_bpf(true) call to
force the native processor, or alternately remove the
processor!(dlp::slow_process_instruction) argument if you intend to test the
compiled BPF program instead; ensure the change references ProgramTest::new,
prefer_bpf(true), and processor!(dlp::slow_process_instruction) so the correct
processor path is exercised.
dlp-api/src/decrypt.rs (1)

133-152: ⚠️ Potential issue | 🟠 Major

Inserted accounts are ignored in pubkey table reconstruction.

The PostDelegationActions::decrypt implementation builds the pubkey table from signers and non_signers only, but PostDelegationActions may also contain inserted_signers and inserted_non_signers fields. If these are non-zero, the compact instruction indices will resolve against the wrong table entries.

This was flagged in a previous review. Until full support is implemented, consider adding a guard:

🛡️ Proposed guard
     fn decrypt(
         self,
         recipient_x25519_pubkey: &[u8; KEY_LEN],
         recipient_x25519_secret: &[u8; KEY_LEN],
     ) -> Result<Self::Output, DecryptError> {
         let actions = self;
+
+        // Guard against unsupported inserted accounts until full support is added
+        if actions.inserted_signers != 0 || actions.inserted_non_signers != 0 {
+            // TODO: Implement proper handling for inserted accounts
+            return Err(DecryptError::InvalidAccountIndex {
+                index: 0,
+                len: 0,
+            });
+        }

         let pubkeys = {
#!/bin/bash
# Check if inserted_signers/inserted_non_signers are used elsewhere
rg -n "inserted_signers|inserted_non_signers" --type rust -C 2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/decrypt.rs` around lines 133 - 152, The decrypt implementation
for PostDelegationActions currently ignores inserted_signers and
inserted_non_signers; add a guard at the start of PostDelegationActions::decrypt
that checks if self.inserted_signers.is_empty() &&
self.inserted_non_signers.is_empty() and if not returns an
Err(DecryptError::Unsupported or another appropriate DecryptError) with a clear
message; this prevents incorrect pubkey table reconstruction until full support
for inserted_* fields is implemented and ensures callers get a deterministic
error instead of silently wrong decryption.
src/processor/fast/delegate_with_actions.rs (1)

148-218: 🧹 Nitpick | 🔵 Trivial

Consider extracting shared PDA seed validation.

This seed validation block (~60 lines) is duplicated in delegate.rs. Extracting it into a shared helper like validate_pda_seeds(seeds, delegated_account, program_id) -> Result<(), ProgramError> would:

  • Reduce duplication
  • Ensure consistent validation across both processors
  • Make maintenance easier

Additionally, the 0 case falls through to the _ arm returning TooManySeeds, which is semantically incorrect (zero seeds is not "too many"). A dedicated 0 => arm with a clearer error would improve diagnostics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 148 - 218, Extract
the PDA seed validation into a shared helper (e.g. fn validate_pda_seeds(seeds:
&[Vec<u8>], delegated: &AccountInfo, program_id: &Address) -> Result<(),
ProgramError>) that contains the current logic: build seeds_to_validate from
args.delegate.seeds, handle the 0 case explicitly (return a clear error like
DlpError::NoSeeds.into() instead of falling through to TooManySeeds), call
Address::find_program_address(seeds_to_validate, program_id).0 and compare to
delegated_account.address(), logging and returning ProgramError::InvalidSeeds on
mismatch; then replace the duplicated block in delegate_with_actions.rs (and
delegate.rs) with a single call to validate_pda_seeds(&args.delegate.seeds,
delegated_account, program_id). Ensure you add the new DlpError::NoSeeds variant
(or an appropriate explicit error) and update imports/usages accordingly.
dlp-api/src/encryption/mod.rs (1)

43-50: ⚠️ Potential issue | 🟠 Major

Don't panic on malformed Ed25519 secret keys.

Line 46 aborts before this Result-returning API can map bad input to EncryptionError::InvalidEd25519SecretKey. Tighten the parameter to &[u8; 64] or return Err(...) on length mismatch.

Suggested fix
 pub fn ed25519_secret_to_x25519(
-    ed25519_secret_key: &[u8],
+    ed25519_secret_key: &[u8; 64],
 ) -> Result<[u8; KEY_LEN], EncryptionError> {
-    assert_eq!(ed25519_secret_key.len(), 64);
     init_sodium()?;

Expected result: docs confirm crypto_sign::SecretKey::from_bytes only accepts a 64-byte Ed25519 secret key.

In libsodium-rs 0.2.0, what exact byte length does crypto_sign::SecretKey::from_bytes accept for an Ed25519 secret key?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/encryption/mod.rs` around lines 43 - 50, The function
ed25519_secret_to_x25519 currently asserts the input length and can panic;
change it to avoid panics by either tightening the parameter type to accept
&[u8; 64] or by validating the slice length at the top of
ed25519_secret_to_x25519 and returning
Err(EncryptionError::InvalidEd25519SecretKey) when ed25519_secret_key.len() !=
64 (before calling init_sodium() and crypto_sign::SecretKey::from_bytes). Ensure
references to KEY_LEN and EncryptionError::InvalidEd25519SecretKey are used so
callers get a proper Result rather than a panic.
dlp-api/src/encrypt.rs (1)

174-195: ⚠️ Potential issue | 🟠 Major

Propagate encryption failures instead of panicking.

encrypt() returns Result, but Lines 184-193 and 205-212 still use expect(...). Any encryption failure aborts the process instead of surfacing EncryptionError to the caller.

Suggested fix
-        let compact_instructions: Vec<MaybeEncryptedInstruction> = self
+        let compact_instructions: Vec<MaybeEncryptedInstruction> = self
             .into_iter()
-            .map(|ix| MaybeEncryptedInstruction {
-                program_id: index_of(&ix.program_id.pubkey),
+            .map(|ix| -> Result<MaybeEncryptedInstruction, EncryptionError> {
+                Ok(MaybeEncryptedInstruction {
+                    program_id: index_of(&ix.program_id.pubkey),

                     accounts: ix
                         .accounts
                         .into_iter()
                         .map(|meta| {
                             let index = index_of(&meta.account_meta.pubkey);
-                            meta.to_compact(index)
-                                .encrypt(validator)
-                                .expect("account metadata encryption failed")
+                            meta.to_compact(index).encrypt(validator)
                         })
-                        .collect(),
+                        .collect::<Result<Vec<_>, _>>()?,

-                data: ix
-                    .data
-                    .encrypt(validator)
-                    .expect("instruction data encryption failed"),
-            })
-            .collect();
+                    data: ix.data.encrypt(validator)?,
+                })
+            })
+            .collect::<Result<Vec<_>, _>>()?;
-                non_signers: non_signers
+                non_signers: non_signers
                     .into_iter()
                     .map(|ns| {
                         EncryptablePubkey {
                             pubkey: ns.account_meta.pubkey,
                             is_encryptable: ns.is_encryptable,
                         }
-                        .encrypt(validator)
-                        .expect("pubkey encryption failed")
+                        .encrypt(validator)
                     })
-                    .collect(),
+                    .collect::<Result<Vec<_>, _>>()?,

Also applies to: 203-213

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/encrypt.rs` around lines 174 - 195, The code currently calls
.encrypt(...).expect(...) inside the mapping that builds
MaybeEncryptedInstruction (symbols: MaybeEncryptedInstruction, index_of,
to_compact, encrypt), which panics on failure; change this to propagate the
EncryptionError by replacing the expects with the ? operator (or map_err(...)
and return Err) and update the enclosing function/method's return type to
Result<..., EncryptionError> so encryption failures bubble up to the caller
instead of aborting; ensure both places noted (the account meta encryption and
the instruction data encryption) use the same error-propagation pattern.
dlp-api/src/instruction_builder/types/encryptable_types.rs (1)

89-98: ⚠️ Potential issue | 🟠 Major

Make to_compact fallible.

This public method accepts any u8, but Line 97 panics for 64..=255. Return a Result / add try_to_compact, or accept a validated compact-index type so callers cannot crash the builder.

Based on learnings: In src/compact/instruction.rs, the program_id field is stored as u8 but is constrained to 6 bits (0-63) by the index_of callback provided by the caller, not by explicit validation in from_instruction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/types/encryptable_types.rs` around lines 89 -
98, The method EncryptableAccountMeta::to_compact must not panic on invalid
indices; make it fallible by either changing its signature to return
Result<dlp::compact::EncryptableAccountMeta, E> or adding a new
try_to_compact(...) that returns Result. Replace the .expect("compact account
index must fit in 6 bits") on AccountMeta::try_new with proper error propagation
(use the Result from try_new and map/propagate an appropriate error type), or
accept a validated compact-index wrapper type instead of raw u8; update all
callers to handle the Result accordingly (propagate ? or convert errors) so no
caller can crash the builder.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.coderabbit.yaml:
- Around line 34-39: The comment and the settings conflict: update the
finishing_touches section so the comment and unit_tests.enabled align; either
set unit_tests.enabled to false to match "Don’t auto-generate docstrings/tests
(reduce chatter)" or change the comment to reflect that unit tests are enabled.
Locate the finishing_touches block and modify the unit_tests.enabled value or
the comment text accordingly, referencing the keys finishing_touches,
docstrings.enabled, and unit_tests.enabled.

In `@dlp-api/src/instruction_builder/commit_diff_from_buffer.rs`:
- Around line 21-22: The doc summary is stale — it says "commit state from
buffer" but the function builds the commit_diff_from_buffer instruction; update
the top-line comment in commit_diff_from_buffer.rs to accurately describe that
it "Builds a commit_diff_from_buffer instruction" (or similar) and ensure the
reference link still points to dlp::processor::process_commit_diff_from_buffer
so the public docs match the exported API name commit_diff_from_buffer.

In `@dlp-api/src/instruction_builder/commit_diff.rs`:
- Around line 21-22: The top-line doc comment is stale: replace the summary
"Builds a commit state instruction." with an accurate description like "Builds a
commit-diff instruction." in the public doc for the commit_diff
function/constructor in commit_diff.rs, ensuring the rustdoc now correctly
refers to the function name commit_diff and still points to
dlp::processor::fast::process_commit_diff for details.

In `@src/compact/mod.rs`:
- Around line 255-266: The current guard only counts newly discovered keys
(signers.len() + non_signers.len()) and ignores existing keys already in
insertable (insertable.signers.len(), insertable.non_signers.len()), which can
let total pubkeys exceed crate::compact::MAX_PUBKEYS and later cause a panic
when index_of adds old_total; update the check to include old_total by computing
old_signers_len = insertable.signers.len() and old_non_signers_len =
insertable.non_signers.len() and assert (signers.len() + non_signers.len() +
old_signers_len + old_non_signers_len) <= crate::compact::MAX_PUBKEYS (or panic
with the same message), and also verify anywhere index_of is used (and where
program_id is stored as u8) that returned indices cannot exceed the 6-bit limit
(0..=63) so callers or index_of validate/clip accordingly.

In `@src/requires.rs`:
- Line 822: The inner conditional attribute #[cfg(all(not(feature =
"unit_test_config"), feature = "processor"))] is redundant because the enclosing
item is already guarded by #[cfg(feature = "processor")] (declared earlier
around line 803); change the inner attribute to only check the unit_test_config
condition (i.e., #[cfg(not(feature = "unit_test_config"))]) so you remove the
duplicate feature check while preserving the unit-test gating for the item
referenced by that attribute.

In `@tests/test_call_handler_v2.rs`:
- Around line 485-496: The test currently executes finalize_call_handler_v2_ix
before the undelegate call so the invalid escrow causes the transaction to abort
early; reorder the instructions so undelegate_call_handler_v2_ix (or
undelegate_ix) is invoked/executed before finalize_call_handler_v2_ix, or
alternatively make the escrow setup valid for finalize by adjusting the
escrow/state used by finalize_call_handler_v2_ix; update the test to ensure
undelegate_call_handler_v2_ix runs and asserts its path before invoking
finalize_call_handler_v2_ix.

In `@tests/test_delegate_with_actions.rs`:
- Around line 1-14: The test imports encryption-only items (e.g., Encryptable,
EncryptableFrom, Decrypt, delegate_with_actions) and must be conditionally
compiled; wrap the test module or file with a feature gate like #[cfg(feature =
"encryption")] (or apply that attribute to the test mod) so the imports and
tests are only compiled when the "encryption" feature is enabled, ensuring
symbols such as DelegateWithActionsArgs, Encryptable, and Decrypt are available.

In `@tests/test_lamports_settlement.rs`:
- Around line 473-476: Update the increase-path tests in
tests/test_lamports_settlement.rs (after calling
dlp_api::instruction_builder::finalize and executing the transaction) to assert
that the validator_fees_vault balance is unchanged and that the validator's
account (args.validator.pubkey()) balance decreased by exactly the delta
(commit_lamports - delegation_record.lamports); in other words, capture
pre-finalize balances for validator and validator_fees_vault, run finalize, then
assert validator_fees_vault_balance_after == validator_fees_vault_balance_before
and validator_balance_after == validator_balance_before - expected_delta so the
extra lamports are proven to come from args.validator rather than the vault
(mirroring the behavior in commit_finalize_internal.rs for the increase path).

In `@tests/test_validator_claim_fees.rs`:
- Around line 50-53: The test references
dlp_api::instruction_builder::validator_claim_fees but
dlp_api::instruction_builder is not imported; add the import statement use
dlp_api::instruction_builder; to the imports at the top of
tests/test_validator_claim_fees.rs so validator_claim_fees resolves (follow the
same import pattern used in test_delegate_with_actions.rs).

---

Outside diff comments:
In `@dlp-api/src/instruction_builder/commit_finalize.rs`:
- Around line 55-61: The accounts list built in commit_finalize currently marks
validator, delegation_record, and validator_fees_vault as readonly but
process_commit_finalize mutates delegation_record.lamports and may transfer
lamports from args.validator or credit validator_fees_vault; update the
AccountMeta entries in the commit_finalize builder so that validator,
delegation_record.0, and validator_fees_vault.0 are created as writable (use
AccountMeta::new instead of AccountMeta::new_readonly) while keeping
delegation_metadata and system_program as readonly as appropriate.

In `@tests/test_commit_finalize.rs`:
- Around line 52-68: Assert that the validator account meta is writable after
building the instruction: after the let (ix, pdas) =
dlp_api::instruction_builder::commit_finalize(...) call, add an assertion like
assert!(pdas[0].is_writable(), "validator meta must be writable"); do the same
in test_commit_finalize_out_of_order; this ensures the validator (account index
0) returned by commit_finalize/commit_finalize_from_buffer is AccountMeta::new
(writable) rather than readonly.

---

Duplicate comments:
In `@Cargo.toml`:
- Around line 76-77: Update the Cargo.toml dependency lines to constrain
solana-sdk to the same upper bound as solana-program (e.g., "<3.0.0") and relax
the rand pin to allow patch updates by changing the exact version "=0.8.5" to a
caret requirement like "^0.8.5"; also apply the same rand change to the
corresponding dev-dependency mentioned on line 84 so all rand entries use the
caret form and solana-sdk aligns with solana-program's version range.
- Line 57: The Cargo.toml has mismatched Solana crate versions: solana-program
is constrained to <3.0.0 while solana-instruction (and dev-dep solana-pubkey)
are pinned to 3.0.0; fix by making all Solana crates use the same major
version—either update the solana-program version constraint to allow 3.x (e.g.
change its requirement to something like >=1.16, <4.0.0) so it aligns with
solana-instruction/solana-pubkey 3.0.0, or downgrade solana-instruction and
solana-pubkey to 2.x so they match the existing solana-program constraint;
update the corresponding version strings in Cargo.toml for the symbols
solana-program, solana-instruction, and solana-pubkey accordingly and run cargo
update to verify resolution.

In `@dlp-api/src/decrypt.rs`:
- Around line 133-152: The decrypt implementation for PostDelegationActions
currently ignores inserted_signers and inserted_non_signers; add a guard at the
start of PostDelegationActions::decrypt that checks if
self.inserted_signers.is_empty() && self.inserted_non_signers.is_empty() and if
not returns an Err(DecryptError::Unsupported or another appropriate
DecryptError) with a clear message; this prevents incorrect pubkey table
reconstruction until full support for inserted_* fields is implemented and
ensures callers get a deterministic error instead of silently wrong decryption.

In `@dlp-api/src/encrypt.rs`:
- Around line 174-195: The code currently calls .encrypt(...).expect(...) inside
the mapping that builds MaybeEncryptedInstruction (symbols:
MaybeEncryptedInstruction, index_of, to_compact, encrypt), which panics on
failure; change this to propagate the EncryptionError by replacing the expects
with the ? operator (or map_err(...) and return Err) and update the enclosing
function/method's return type to Result<..., EncryptionError> so encryption
failures bubble up to the caller instead of aborting; ensure both places noted
(the account meta encryption and the instruction data encryption) use the same
error-propagation pattern.

In `@dlp-api/src/encryption/mod.rs`:
- Around line 43-50: The function ed25519_secret_to_x25519 currently asserts the
input length and can panic; change it to avoid panics by either tightening the
parameter type to accept &[u8; 64] or by validating the slice length at the top
of ed25519_secret_to_x25519 and returning
Err(EncryptionError::InvalidEd25519SecretKey) when ed25519_secret_key.len() !=
64 (before calling init_sodium() and crypto_sign::SecretKey::from_bytes). Ensure
references to KEY_LEN and EncryptionError::InvalidEd25519SecretKey are used so
callers get a proper Result rather than a panic.

In `@dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs`:
- Around line 49-51: The validator account is incorrectly marked readonly in the
commit_finalize account list; change the AccountMeta for validator from
AccountMeta::new_readonly(validator, true) to AccountMeta::new(validator, true)
so the validator (index 0) is explicitly writable for commit_finalize paths that
may transfer lamports to delegated_account.
- Around line 16-17: The doc comment incorrectly references
process_commit_diff_from_buffer for the builder that constructs a
commit_finalize_from_buffer instruction; update the documentation to reference
the correct processing function (e.g., process_commit_finalize_from_buffer) or
the instruction name commit_finalize_from_buffer to match the discriminator used
in this file (see discriminator at line with commit_finalize_from_buffer) so the
docs and code are consistent.

In `@dlp-api/src/instruction_builder/commit_state_from_buffer.rs`:
- Around line 43-46: The validator AccountMeta is currently created as readonly
(AccountMeta::new_readonly(validator, true)) which will fail when the on-chain
CommitStateFromBuffer flow needs to create PDAs or move lamports; change the
validator AccountMeta to be writable by using AccountMeta::new(validator, true)
in commit_state_from_buffer.rs (and apply the same pattern in other builders
like commit_finalize_internal.rs and commit_finalize_from_buffer.rs) so the
validator is explicitly writable when passed to the instruction builder.

In `@dlp-api/src/instruction_builder/commit_state.rs`:
- Around line 42-45: The validator AccountMeta in the Instruction construction
inside commit_state.rs is created as readonly
(AccountMeta::new_readonly(validator, true)), which leaves index 0 non-writable
and breaks cases where process_commit_state uses the validator as the payer;
update the AccountMeta for the validator to be writable (use the writable
constructor for the validator account at index 0 so it is explicitly mutable and
still a signer) so the commit-side PDA creation/update can write to it.

In `@dlp-api/src/instruction_builder/finalize.rs`:
- Around line 33-35: In the finalize instruction builder update the AccountMeta
for the validator so it is writable: replace or change the
AccountMeta::new_readonly(validator, true) entry used in accounts within
finalize to use AccountMeta::new(validator, true) (leaving the delegated_account
AccountMeta::new(delegated_account, false) as-is) so the validator account
(referenced by validator) is marked writable for subsequent close_pda transfers.

In `@dlp-api/src/instruction_builder/types/encryptable_types.rs`:
- Around line 89-98: The method EncryptableAccountMeta::to_compact must not
panic on invalid indices; make it fallible by either changing its signature to
return Result<dlp::compact::EncryptableAccountMeta, E> or adding a new
try_to_compact(...) that returns Result. Replace the .expect("compact account
index must fit in 6 bits") on AccountMeta::try_new with proper error propagation
(use the Result from try_new and map/propagate an appropriate error type), or
accept a validated compact-index wrapper type instead of raw u8; update all
callers to handle the Result accordingly (propagate ? or convert errors) so no
caller can crash the builder.

In `@Makefile`:
- Around line 7-8: The lint Makefile target currently runs `cargo clippy
--features sdk,program -- -D warnings` but omits test modules/examples/benches;
update the `lint` target (the Makefile `lint` rule) to include `--all-targets`
in the cargo clippy invocation so clippy also checks #[cfg(test)] code,
examples, benches, and tests while preserving the existing `--features
sdk,program` and `-D warnings` flags.

In `@src/compact/instruction.rs`:
- Around line 5-22: from_instruction currently accepts any u8 from the index_of
callback, so add a runtime check to ensure the returned program_id is < 64 and
surface failure instead of silently storing invalid values: change
Instruction::from_instruction to return Result<Instruction, SomeError> (or panic
with a clear message if you prefer), validate the value of
index_of(ix.program_id, false) < 64 before constructing Instruction, and
propagate/return an error when it violates the 6-bit invariant; additionally,
implement a custom Deserialize (or add a post-deserialize check) for Instruction
that rejects deserialized program_id >= 64 so the invariant is enforced at the
parsing boundary as well (references: Instruction, from_instruction, program_id,
Deserialize, AccountMeta).

In `@src/processor/delegate_ephemeral_balance.rs`:
- Around line 91-102: Recompute the expected PDAs using
delegate_buffer_pda_from_delegated_account_and_owner_program,
delegation_record_pda_from_delegated_account, and
delegation_metadata_pda_from_delegated_account and immediately validate that the
provided accounts (delegate_buffer, delegation_record, delegation_metadata) have
matching pubkeys; if any mismatch, return a deterministic program error (e.g.,
an InvalidAccount or custom error) before performing the self-CPI invoke to
avoid failing inside the inner invoke.
- Around line 107-119: The constructed inner Instruction marks payer,
ephemeral_balance_account, delegate_buffer_pda, delegation_record_pda, and
delegation_metadata_pda as writable but the processor does not validate those
writable privileges before calling invoke_signed; update the code in
delegate_ephemeral_balance.rs to explicitly check each corresponding AccountInfo
(payer, ephemeral_balance_account, delegate_buffer, delegation_record,
delegation_metadata) has is_writable == true and return a clear error (e.g.,
ProgramError::InvalidAccountData or a custom PrivilegeEscalation error) if any
are readonly, and apply the same checks for the second invoke_signed call region
(the block corresponding to lines 121-134) so CPI failures are caught locally
with a descriptive error instead of a generic privilege error.

In `@src/processor/fast/delegate_with_actions.rs`:
- Around line 148-218: Extract the PDA seed validation into a shared helper
(e.g. fn validate_pda_seeds(seeds: &[Vec<u8>], delegated: &AccountInfo,
program_id: &Address) -> Result<(), ProgramError>) that contains the current
logic: build seeds_to_validate from args.delegate.seeds, handle the 0 case
explicitly (return a clear error like DlpError::NoSeeds.into() instead of
falling through to TooManySeeds), call
Address::find_program_address(seeds_to_validate, program_id).0 and compare to
delegated_account.address(), logging and returning ProgramError::InvalidSeeds on
mismatch; then replace the duplicated block in delegate_with_actions.rs (and
delegate.rs) with a single call to validate_pda_seeds(&args.delegate.seeds,
delegated_account, program_id). Ensure you add the new DlpError::NoSeeds variant
(or an appropriate explicit error) and update imports/usages accordingly.

In `@tests/test_call_handler.rs`:
- Around line 256-261: The test currently calls ProgramTest::new(...,
processor!(dlp::slow_process_instruction)) but then sets prefer_bpf(true), which
causes the BPF artifact to be chosen over the supplied native processor; update
the test so it actually exercises dlp::slow_process_instruction by either
removing the prefer_bpf(true) call to force the native processor, or alternately
remove the processor!(dlp::slow_process_instruction) argument if you intend to
test the compiled BPF program instead; ensure the change references
ProgramTest::new, prefer_bpf(true), and
processor!(dlp::slow_process_instruction) so the correct processor path is
exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8eb24b94-e0a9-44a1-b9c3-10ea47638914

📥 Commits

Reviewing files that changed from the base of the PR and between 95ca88d and a53b394.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • tests/integration/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (85)
  • .coderabbit.yaml
  • .github/workflows/run-tests.yml
  • Cargo.toml
  • Makefile
  • dlp-api/Cargo.toml
  • dlp-api/src/cpi/delegate_with_actions.rs
  • dlp-api/src/cpi/mod.rs
  • dlp-api/src/decrypt.rs
  • dlp-api/src/encrypt.rs
  • dlp-api/src/encryption/mod.rs
  • dlp-api/src/instruction_builder/call_handler.rs
  • dlp-api/src/instruction_builder/call_handler_v2.rs
  • dlp-api/src/instruction_builder/close_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/close_validator_fees_vault.rs
  • dlp-api/src/instruction_builder/commit_diff.rs
  • dlp-api/src/instruction_builder/commit_diff_from_buffer.rs
  • dlp-api/src/instruction_builder/commit_finalize.rs
  • dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs
  • dlp-api/src/instruction_builder/commit_state.rs
  • dlp-api/src/instruction_builder/commit_state_from_buffer.rs
  • dlp-api/src/instruction_builder/delegate.rs
  • dlp-api/src/instruction_builder/delegate_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/delegate_with_actions.rs
  • dlp-api/src/instruction_builder/finalize.rs
  • dlp-api/src/instruction_builder/init_protocol_fees_vault.rs
  • dlp-api/src/instruction_builder/init_validator_fees_vault.rs
  • dlp-api/src/instruction_builder/mod.rs
  • dlp-api/src/instruction_builder/protocol_claim_fees.rs
  • dlp-api/src/instruction_builder/top_up_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/types/encryptable_types.rs
  • dlp-api/src/instruction_builder/types/mod.rs
  • dlp-api/src/instruction_builder/undelegate.rs
  • dlp-api/src/instruction_builder/undelegate_confined_account.rs
  • dlp-api/src/instruction_builder/validator_claim_fees.rs
  • dlp-api/src/instruction_builder/whitelist_validator_for_program.rs
  • dlp-api/src/lib.rs
  • src/args/delegate_with_actions.rs
  • src/args/mod.rs
  • src/compact/account_meta.rs
  • src/compact/instruction.rs
  • src/compact/mod.rs
  • src/consts.rs
  • src/diff/algorithm.rs
  • src/discriminator.rs
  • src/error.rs
  • src/lib.rs
  • src/processor/delegate_ephemeral_balance.rs
  • src/processor/fast/commit_state.rs
  • src/processor/fast/delegate.rs
  • src/processor/fast/delegate_with_actions.rs
  • src/processor/fast/finalize.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/undelegate.rs
  • src/processor/fast/undelegate_confined_account.rs
  • src/processor/fast/utils/mod.rs
  • src/requires.rs
  • src/state/utils/try_from_bytes.rs
  • tests/integration/programs/test-delegation/Cargo.toml
  • tests/test_call_handler.rs
  • tests/test_call_handler_v2.rs
  • tests/test_cleartext_with_insertable_encrypted.rs
  • tests/test_close_validator_fees_vault.rs
  • tests/test_commit_fees_on_undelegation.rs
  • tests/test_commit_finalize.rs
  • tests/test_commit_finalize_from_buffer.rs
  • tests/test_commit_on_curve.rs
  • tests/test_commit_state.rs
  • tests/test_commit_state_from_buffer.rs
  • tests/test_commit_state_with_program_config.rs
  • tests/test_commit_undelegate_zero_lamports_system_owned.rs
  • tests/test_delegate_on_curve.rs
  • tests/test_delegate_with_actions.rs
  • tests/test_delegation_confined_accounts.rs
  • tests/test_finalize.rs
  • tests/test_init_fees_vault.rs
  • tests/test_init_validator_fees_vault.rs
  • tests/test_lamports_settlement.rs
  • tests/test_protocol_claim_fees.rs
  • tests/test_top_up.rs
  • tests/test_undelegate.rs
  • tests/test_undelegate_confined_account.rs
  • tests/test_undelegate_on_curve.rs
  • tests/test_undelegate_without_commit.rs
  • tests/test_validator_claim_fees.rs
  • tests/test_whitelist_validator_for_program.rs
💤 Files with no reviewable changes (1)
  • src/processor/fast/utils/mod.rs

Comment on lines +34 to +39
# Don’t auto-generate docstrings/tests (reduce chatter)
finishing_touches:
docstrings:
enabled: false
unit_tests:
enabled: true
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

This note and setting disagree on whether unit-test generation is on.

Line 34 says this section is reducing chatter by not generating tests, but Line 39 keeps unit_tests.enabled: true. Either flip the setting or rewrite the comment so the file is not self-contradictory.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.coderabbit.yaml around lines 34 - 39, The comment and the settings
conflict: update the finishing_touches section so the comment and
unit_tests.enabled align; either set unit_tests.enabled to false to match "Don’t
auto-generate docstrings/tests (reduce chatter)" or change the comment to
reflect that unit tests are enabled. Locate the finishing_touches block and
modify the unit_tests.enabled value or the comment text accordingly, referencing
the keys finishing_touches, docstrings.enabled, and unit_tests.enabled.

@magicblock-labs magicblock-labs deleted a comment from coderabbitai bot Mar 12, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 12, 2026

Warning

Ignoring CodeRabbit configuration file changes. For security, only the configuration from the base branch is applied for open source repositories.

Walkthrough

This PR introduces the dlp-api crate as a new workspace member, adding encryption-based delegation instruction builders with CPI support. It implements end-to-end encryption/decryption for post-delegation actions, adds compact instruction encoding, introduces the delegate_with_actions processor, refactors instruction builders to use external module paths, and adjusts feature gating for processor/encryption separation.

Changes

Cohort / File(s) Summary
Configuration & Build Setup
.coderabbit.yaml, Cargo.toml, Makefile, .github/workflows/run-tests.yml
Adds CodeRabbit AI review config; configures workspace with dlp-api member; introduces processor, diff, and encryption features; adds lint target with clippy checks.
DLP-API Encryption Module
dlp-api/src/encryption/mod.rs, dlp-api/src/encrypt.rs, dlp-api/src/decrypt.rs
Implements libsodium-based encryption/decryption for Ed25519→X25519 key conversion, sealed-box encryption/decryption, and trait implementations for encrypting/decrypting Pubkeys, AccountMeta, instruction data, and full PostDelegationActions. Includes round-trip tests and error handling via EncryptionError enum.
DLP-API CPI & Instruction Builders
dlp-api/src/cpi/..., dlp-api/src/instruction_builder/delegate_with_actions.rs, dlp-api/src/instruction_builder/types/..., dlp-api/src/instruction_builder/mod.rs, dlp-api/src/lib.rs
Adds CPI function for delegate_with_actions; introduces Encryptable/EncryptableFrom/Encrypt traits; defines PostDelegationInstruction and EncryptableAccountMeta for granular instruction encryption; creates instruction builder for encrypted delegation actions.
DLP-API Core & Manifest
dlp-api/Cargo.toml, dlp-api/src/args/delegate_with_actions.rs
Configures dlp-api crate with encryption feature, solana-sdk deps, and borsh serialization; defines DelegateWithActionsArgs, PostDelegationActions, and MaybeEncrypted* types for representing encrypted/cleartext instruction components.
Instruction Builder Path Refactoring
dlp-api/src/instruction_builder/call_handler.rs, .../call_handler_v2.rs, .../close_*.rs, .../commit_*.rs, .../delegate.rs, .../finalize.rs, .../init_*.rs, .../protocol_claim_fees.rs, .../top_up_ephemeral_balance.rs, .../undelegate*.rs, .../validator_claim_fees.rs, .../whitelist_validator_for_program.rs
Systematically updates imports and program_id references from crate:: to dlp:: module paths; changes validator AccountMeta from readonly to writable in several builders; updates doc references accordingly.
Core Program - Compact Encoding
src/compact/mod.rs, src/compact/account_meta.rs, src/compact/instruction.rs, src/args/mod.rs
Introduces compact AccountMeta with 6-bit index and flag bits (signer/writable); defines ClearText trait for converting instructions to PostDelegationActions; implements ClearTextWithInsertable for merging encrypted/cleartext instruction sequences with index remapping and deduplication.
Core Program - Processor & Delegate Actions
src/processor/fast/delegate_with_actions.rs, src/processor/delegate_ephemeral_balance.rs, src/processor/fast/mod.rs
Adds process_delegate_with_actions to handle encrypted post-delegation actions with account validation, PDA derivation, and metadata initialization; refactors delegate_ephemeral_balance to construct instructions directly rather than use builders.
Core Program - Module Structure & Features
src/lib.rs, src/discriminator.rs, src/error.rs, src/requires.rs, src/consts.rs, src/state/utils/try_from_bytes.rs, src/diff/algorithm.rs
Introduces compact, requires, and public discriminator modules; adds DelegateWithActions discriminator variant; gates pinocchio-related error impls to pinocchio-rt feature; makes requires functions crate-private and processor-gated; adjusts DELEGATION_PROGRAM_DATA_ID derivation with two-seed PDA; updates slice length calculations in state parsing.
Processor Import Reorganization
src/processor/fast/commit_state.rs, .../delegate.rs, .../finalize.rs, .../undelegate*.rs, src/processor/fast/utils/mod.rs
Flattens nested imports by moving pda and requires items to top-level module paths; removes requires module declaration from fast/utils. No behavioral changes.
Test Updates
tests/test_call_handler.rs, tests/test_call_handler_v2.rs, tests/test_commit_*.rs, tests/test_delegate_*.rs, tests/test_finalize.rs, tests/test_init_*.rs, tests/test_undelegate*.rs, tests/test_*.rs (all remaining test files)
Updates instruction builder calls from dlp::instruction_builder to dlp_api::instruction_builder; refactors test setup to use slow_process_instruction; adds test_delegate_with_actions with comprehensive encryption/decryption validation; adds test_cleartext_with_insertable_encrypted for compact instruction merging.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Suggested reviewers

  • GabrielePicco
  • taco-paco
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Add DelegateWithActions' clearly summarizes the main feature being introduced, aligning with the PR's primary objective to add the DelegateWithActions instruction as described in the summary and objectives.
Description check ✅ Passed The PR description adequately explains the problem (introducing DelegateWithActions), solution (adding offchain/onchain APIs and extracting dlp-api crate), and acknowledges the implementation is initial. However, it is missing the structured format elements (Status/Type/Issue table, Before & After screenshots, Deploy Notes with specific new dependencies/scripts) defined in the repository's description template.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch snawaz/delegate-with-actions
📝 Coding Plan for PR comments
  • Generate coding plan

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (11)
src/requires.rs (1)

822-822: 🧹 Nitpick | 🔵 Trivial

Redundant feature check in inner cfg.

The feature = "processor" condition is redundant here since the entire function is already gated by #[cfg(feature = "processor")] on line 803.

♻️ Suggested simplification
-    #[cfg(all(not(feature = "unit_test_config"), feature = "processor"))]
+    #[cfg(not(feature = "unit_test_config"))]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/requires.rs` at line 822, The inner cfg attribute currently uses
#[cfg(all(not(feature = "unit_test_config"), feature = "processor"))] which
redundantly repeats the outer #[cfg(feature = "processor")] already applied to
the surrounding function; replace the inner attribute with #[cfg(not(feature =
"unit_test_config"))] so the code is only gated by the unit_test_config negation
while preserving the outer processor guard (reference the inner attribute and
the outer #[cfg(feature = "processor")] on the function to locate the change).
src/processor/fast/delegate_with_actions.rs (2)

287-292: ⚠️ Potential issue | 🟡 Minor

Buffer-to-account copy lacks length validation.

The copy_from_slice at line 291 will panic if delegate_buffer_account and delegated_account have mismatched data lengths. While the author noted this is an unlikely scenario and resizing would be semantically wrong, a defensive length check returning an error would prevent runtime panics:

if delegate_buffer_data.len() != delegated_data.len() {
    return Err(DlpError::InvalidDataLength.into());
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 287 - 292, The
buffer-to-account copy can panic when lengths differ: in the block that borrows
delegated_account and delegate_buffer_account (around the copy_from_slice call),
add a defensive length check comparing delegated_data.len() and
delegate_buffer_data.len(); if they differ return an error (e.g.,
Err(DlpError::InvalidDataLength.into())) instead of calling copy_from_slice.
Keep the borrow calls (delegated_account.try_borrow_mut(),
delegate_buffer_account.try_borrow()) and perform the length check before
performing the copy_from_slice to avoid runtime panics.

148-218: 🧹 Nitpick | 🔵 Trivial

Seed validation logic is correct but duplicated.

This block duplicates the seed validation from delegate.rs. As noted in previous reviews, extracting a shared helper like validate_pda_seeds(seeds, delegated_account, program_id) would reduce duplication.

Additionally, seeds.len() == 0 falls through to the _ => arm returning TooManySeeds, which could be misleading for callers debugging zero-seed cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/processor/fast/delegate_with_actions.rs` around lines 148 - 218, Extract
the duplicated seed-validation into a shared helper (e.g., validate_pda_seeds)
and replace the inline block in delegate_with_actions.rs with a call to that
helper; the helper should accept (&[Vec<u8>] or &[&[u8]], delegated_account:
&Address, program_id: &Address) and use Address::find_program_address to compare
derived_pda to delegated_account.address(), logging mismatches and returning
ProgramError::InvalidSeeds on mismatch. Also update the helper to explicitly
handle zero-length seeds (do not map it to DlpError::TooManySeeds) by returning
a clearer error (e.g., DlpError::TooFewSeeds or a new variant) instead of the
current default arm, and update call sites in delegate.rs and
delegate_with_actions.rs to use the new helper; keep is_on_curve_fast check and
program_id resolution logic outside or pass program_id into the helper.
tests/test_lamports_settlement.rs (1)

239-252: 🧹 Nitpick | 🔵 Trivial

Consider strengthening assertions for the balance-increase path.

The test asserts that validator_vault.lamports >= Rent::default().minimum_balance(0) but doesn't verify that the vault balance remained unchanged or that the validator's wallet decreased by the expected delta. This applies to all *_after_balance_increase* test variants.

Based on learnings, for the lamport increase path, extra lamports are transferred from the validator's wallet (not the vault). Capturing pre-finalize balances and asserting exact post-finalize values would make these tests more robust against regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_lamports_settlement.rs` around lines 239 - 252, Capture and record
pre-finalize balances for the validator wallet (the account queried via
banks.get_account(authority.pubkey() or its associated key) and for the
validator vault (queried via
validator_fees_vault_pda_from_validator(&authority.pubkey())), then after
finalize assert exact expected deltas: verify delegated_account.lamports ==
new_delegated_account_lamports as before, assert validator_wallet_post ==
validator_wallet_pre - expected_delta, and assert validator_vault_post ==
validator_vault_pre (or unchanged aside from Rent::default().minimum_balance(0))
so the test checks that the extra lamports came from the validator wallet and
the vault did not absorb the increase. Ensure you reference and update the
existing variables/declarations around delegated_account, validator_vault,
validator_fees_vault_pda_from_validator, Rent, and banks.get_account calls when
adding the pre/post balance captures and exact assertions.
tests/test_cleartext_with_insertable_encrypted.rs (1)

78-80: 🧹 Nitpick | 🔵 Trivial

Consider documenting the encryption detection heuristic.

The is_encrypted closure uses !ix.data.suffix.as_bytes().is_empty() to determine encryption status. A brief inline comment explaining this convention would improve clarity for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_cleartext_with_insertable_encrypted.rs` around lines 78 - 80, The
closure is_encrypted currently uses the heuristic
!ix.data.suffix.as_bytes().is_empty() to detect encryption but lacks
explanation; add a concise inline comment next to the is_encrypted closure
(referencing is_encrypted, dlp::args::MaybeEncryptedInstruction, and
ix.data.suffix) that explains the convention: a non-empty suffix indicates the
presence of encryption metadata/marker bytes so the instruction should be
treated as encrypted.
dlp-api/src/instruction_builder/types/encryptable_types.rs (1)

89-99: ⚠️ Potential issue | 🟠 Major

Make to_compact fallible.

This helper accepts any u8, but 64..=255 still trips the expect(...). Please return a Result (or add try_to_compact) so oversize pubkey tables surface as normal builder errors instead of panicking. Based on learnings: the compact pubkey table relies on a 6-bit 0..=63 index invariant that callers must preserve explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/types/encryptable_types.rs` around lines 89 -
99, Change EncryptableAccountMeta::to_compact to be fallible by returning a
Result (or add a new try_to_compact) instead of panicking; remove the
.expect(...) and validate that the provided index fits in 6 bits (0..=63) before
constructing dlp::compact::AccountMeta::try_new, returning an appropriate error
if the index is out of range so oversize pubkey table indices surface as normal
builder errors rather than panics.
dlp-api/src/instruction_builder/delegate_with_actions.rs (1)

21-33: ⚠️ Potential issue | 🟠 Major

Return a fallible builder API here.

A missing delegate.validator and an encryption failure are ordinary off-chain input errors. Panicking on both makes this public builder hard to recover from; please return Result<Instruction, _> and propagate the failures instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/instruction_builder/delegate_with_actions.rs` around lines 21 -
33, Change the public builder fn delegate_with_actions to return a fallible
result (e.g. Result<Instruction, E>) instead of panicking: replace the
unwrap/expect on delegate.validator by returning an appropriate Err when
validator is None, and propagate the error from actions.encrypt rather than
expect-ing it; update signatures and all callers of delegate_with_actions
accordingly, and use meaningful error variants (or Box<dyn Error>/thiserror) to
surface both the missing validator and encryption failures (referencing
delegate_with_actions, delegate.validator, encrypt_key, and actions.encrypt).
dlp-api/src/decrypt.rs (1)

141-152: ⚠️ Potential issue | 🟠 Major

Reject inserted-account payloads until the full pubkey table is supported.

This table is rebuilt from signers and decrypted non_signers only. If either inserted_signers or inserted_non_signers is nonzero, every subsequent index lookup can resolve against the wrong key; fail fast here unless the helper is updated to prepend the inserted pubkeys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/decrypt.rs` around lines 141 - 152, Before reconstructing the
pubkey table, reject payloads that contain any inserted accounts: check
actions.inserted_signers and actions.inserted_non_signers and return an error if
either is nonzero; do this in the scope that builds pubkeys (the block that
reads actions.signers and iterates actions.non_signers with
decrypt(recipient_x25519_pubkey, recipient_x25519_secret)) so the function fails
fast until the helper is updated to handle prepended inserted pubkeys.
dlp-api/src/encryption/mod.rs (1)

43-50: ⚠️ Potential issue | 🟠 Major

Don't assert_eq! inside this Result API.

Wrong-length Ed25519 secret keys currently panic before EncryptionError::InvalidEd25519SecretKey can be returned. Tighten the signature to &[u8; 64] or do an explicit length check that maps to Err(...) so invalid input stays recoverable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/encryption/mod.rs` around lines 43 - 50, The function
ed25519_secret_to_x25519 currently uses assert_eq! which panics on wrong-length
input; replace it with an explicit runtime length check that returns
Err(EncryptionError::InvalidEd25519SecretKey) when ed25519_secret_key.len() !=
64 (or tighten the signature to accept &[u8; 64] if you prefer a compile-time
check). After the check, keep the existing init_sodium()? and
crypto_sign::SecretKey::from_bytes(...) logic (mapping its error to
EncryptionError::InvalidEd25519SecretKey) so invalid input is handled as a
recoverable error rather than panicking.
src/lib.rs (1)

4-12: 🛠️ Refactor suggestion | 🟠 Major

Reinstate or delete the commented feature-exclusivity guard.

Leaving the old compile_error! block commented out makes the sdk/program contract ambiguous at the crate root. If the exclusivity rule still matters, restore it; otherwise remove the dead block and enforce the rule somewhere explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib.rs` around lines 4 - 12, The commented-out feature-exclusivity guard
(the compile_error! blocks referring to features "sdk" and "program") must be
resolved: either restore the guard by uncommenting and re-enabling the two
cfg+compile_error! checks (the #[cfg(all(feature = "sdk", feature = "program"))]
and #[cfg(all(not(feature = "sdk"), not(feature = "program")))]
compile_error!(...)) in src/lib.rs, or remove the dead commented block entirely
and enforce the mutual-exclusivity rule elsewhere (e.g., a build.rs check or
CI/Cargo feature documentation) so the contract for "sdk"/"program" is explicit
and not ambiguous.
dlp-api/src/encrypt.rs (1)

84-89: ⚠️ Potential issue | 🟠 Major

Don't panic from a fallible encryption path.

This impl advertises Result<_, EncryptionError>, but malformed action sets, table overflow, and nested encryption failures still reach assert!, panic!, and expect(...). Please convert those branches into Err(...) so callers can handle bad inputs instead of unwinding.

Also applies to: 104-107, 153-160, 174-213

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dlp-api/src/encrypt.rs` around lines 84 - 89, The code currently uses
assert!/panic!/expect in encrypt.rs (notably the add_to_signers closure) which
can unwind despite the public API returning Result; change those assertions and
any expect/panic sites (including the other branches handling action-set
parsing, table overflow, and nested encryption failures) to return
Err(EncryptionError::...) instead, e.g. validate meta.account_meta.is_signer and
meta.is_encryptable and on failure return a descriptive EncryptionError variant,
convert expect(...) calls to map_err(|e| EncryptionError::... e) and propagate
errors with ? or by returning Err so callers can handle them; update the
affected closures/functions (e.g. add_to_signers and the functions that parse
action sets / allocate table slots / perform nested encryption) to return Result
where needed and propagate the new errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/run-tests.yml:
- Around line 53-56: The CI step named "Run clippy" currently runs `cargo clippy
-- --deny=warnings` which doesn't enable the feature flags used by the Makefile
lint target; update that step to invoke the same target as local dev by
replacing the command with `make lint` (or alternately run `cargo clippy
--features "sdk program" -- --deny=warnings`) so CI and local linting use the
same feature set.

In `@dlp-api/src/encrypt.rs`:
- Around line 82-100: The collected signers logic (the signers Vec and the
add_to_signers closure handling EncryptableAccountMeta) must return readonly
AccountMetas so we don't widen the outer instruction's write set; change the
code that pushes/updates signers to ensure entries are created as
AccountMeta::new_readonly(pubkey, true) and do not propagate is_writable (remove
the found.is_writable |= ... update), mirroring the approach used in
DelegateWithActions where action signers are AccountMeta::new_readonly(...,
true); apply the same fix to the other similar blocks referenced (around lines
141-150 and 197-218) that aggregate action signers.

In `@dlp-api/src/instruction_builder/types/encryptable_types.rs`:
- Around line 18-42: The Instruction::with_encryption implementation currently
marks every account encryptable which produces invalid PostDelegationInstruction
when an account is a signer; update with_encryption (the Encryptable impl for
Instruction) to leave signer AccountMeta entries as cleartext by checking each
account's is_signer flag when mapping (use .encrypted() only for non-signers,
.cleartext() for signers), and mirror the same change in the corresponding block
referenced at lines 48-59 so signed instructions do not get encrypted account
metas; keep program_id and data logic unchanged except for using the same
signer-aware mapping for accounts.

In `@Makefile`:
- Around line 7-8: The lint target currently runs "cargo clippy" only on the
root package; update the Makefile target named "lint" to pass the --workspace
flag to cargo clippy (e.g., change the command invoked by the lint recipe that
currently calls "cargo clippy --features sdk,program -- -D warnings" so it
includes "--workspace" as "cargo clippy --workspace --features sdk,program -- -D
warnings") so that the entire workspace (including dlp-api) is linted.

In `@src/diff/algorithm.rs`:
- Line 561: Replace the explicit typed empty-slice assertion with a simple
emptiness check: locate the assertion comparing unwritten to "&mut [] as &mut
[u8]" (the assert_eq!(unwritten, &mut [] as &mut [u8]) line) and change it to
assert!(unwritten.is_empty()) so the intent is clearer and the code is simpler.

---

Duplicate comments:
In `@dlp-api/src/decrypt.rs`:
- Around line 141-152: Before reconstructing the pubkey table, reject payloads
that contain any inserted accounts: check actions.inserted_signers and
actions.inserted_non_signers and return an error if either is nonzero; do this
in the scope that builds pubkeys (the block that reads actions.signers and
iterates actions.non_signers with decrypt(recipient_x25519_pubkey,
recipient_x25519_secret)) so the function fails fast until the helper is updated
to handle prepended inserted pubkeys.

In `@dlp-api/src/encrypt.rs`:
- Around line 84-89: The code currently uses assert!/panic!/expect in encrypt.rs
(notably the add_to_signers closure) which can unwind despite the public API
returning Result; change those assertions and any expect/panic sites (including
the other branches handling action-set parsing, table overflow, and nested
encryption failures) to return Err(EncryptionError::...) instead, e.g. validate
meta.account_meta.is_signer and meta.is_encryptable and on failure return a
descriptive EncryptionError variant, convert expect(...) calls to map_err(|e|
EncryptionError::... e) and propagate errors with ? or by returning Err so
callers can handle them; update the affected closures/functions (e.g.
add_to_signers and the functions that parse action sets / allocate table slots /
perform nested encryption) to return Result where needed and propagate the new
errors.

In `@dlp-api/src/encryption/mod.rs`:
- Around line 43-50: The function ed25519_secret_to_x25519 currently uses
assert_eq! which panics on wrong-length input; replace it with an explicit
runtime length check that returns Err(EncryptionError::InvalidEd25519SecretKey)
when ed25519_secret_key.len() != 64 (or tighten the signature to accept &[u8;
64] if you prefer a compile-time check). After the check, keep the existing
init_sodium()? and crypto_sign::SecretKey::from_bytes(...) logic (mapping its
error to EncryptionError::InvalidEd25519SecretKey) so invalid input is handled
as a recoverable error rather than panicking.

In `@dlp-api/src/instruction_builder/delegate_with_actions.rs`:
- Around line 21-33: Change the public builder fn delegate_with_actions to
return a fallible result (e.g. Result<Instruction, E>) instead of panicking:
replace the unwrap/expect on delegate.validator by returning an appropriate Err
when validator is None, and propagate the error from actions.encrypt rather than
expect-ing it; update signatures and all callers of delegate_with_actions
accordingly, and use meaningful error variants (or Box<dyn Error>/thiserror) to
surface both the missing validator and encryption failures (referencing
delegate_with_actions, delegate.validator, encrypt_key, and actions.encrypt).

In `@dlp-api/src/instruction_builder/types/encryptable_types.rs`:
- Around line 89-99: Change EncryptableAccountMeta::to_compact to be fallible by
returning a Result (or add a new try_to_compact) instead of panicking; remove
the .expect(...) and validate that the provided index fits in 6 bits (0..=63)
before constructing dlp::compact::AccountMeta::try_new, returning an appropriate
error if the index is out of range so oversize pubkey table indices surface as
normal builder errors rather than panics.

In `@src/lib.rs`:
- Around line 4-12: The commented-out feature-exclusivity guard (the
compile_error! blocks referring to features "sdk" and "program") must be
resolved: either restore the guard by uncommenting and re-enabling the two
cfg+compile_error! checks (the #[cfg(all(feature = "sdk", feature = "program"))]
and #[cfg(all(not(feature = "sdk"), not(feature = "program")))]
compile_error!(...)) in src/lib.rs, or remove the dead commented block entirely
and enforce the mutual-exclusivity rule elsewhere (e.g., a build.rs check or
CI/Cargo feature documentation) so the contract for "sdk"/"program" is explicit
and not ambiguous.

In `@src/processor/fast/delegate_with_actions.rs`:
- Around line 287-292: The buffer-to-account copy can panic when lengths differ:
in the block that borrows delegated_account and delegate_buffer_account (around
the copy_from_slice call), add a defensive length check comparing
delegated_data.len() and delegate_buffer_data.len(); if they differ return an
error (e.g., Err(DlpError::InvalidDataLength.into())) instead of calling
copy_from_slice. Keep the borrow calls (delegated_account.try_borrow_mut(),
delegate_buffer_account.try_borrow()) and perform the length check before
performing the copy_from_slice to avoid runtime panics.
- Around line 148-218: Extract the duplicated seed-validation into a shared
helper (e.g., validate_pda_seeds) and replace the inline block in
delegate_with_actions.rs with a call to that helper; the helper should accept
(&[Vec<u8>] or &[&[u8]], delegated_account: &Address, program_id: &Address) and
use Address::find_program_address to compare derived_pda to
delegated_account.address(), logging mismatches and returning
ProgramError::InvalidSeeds on mismatch. Also update the helper to explicitly
handle zero-length seeds (do not map it to DlpError::TooManySeeds) by returning
a clearer error (e.g., DlpError::TooFewSeeds or a new variant) instead of the
current default arm, and update call sites in delegate.rs and
delegate_with_actions.rs to use the new helper; keep is_on_curve_fast check and
program_id resolution logic outside or pass program_id into the helper.

In `@src/requires.rs`:
- Line 822: The inner cfg attribute currently uses #[cfg(all(not(feature =
"unit_test_config"), feature = "processor"))] which redundantly repeats the
outer #[cfg(feature = "processor")] already applied to the surrounding function;
replace the inner attribute with #[cfg(not(feature = "unit_test_config"))] so
the code is only gated by the unit_test_config negation while preserving the
outer processor guard (reference the inner attribute and the outer #[cfg(feature
= "processor")] on the function to locate the change).

In `@tests/test_cleartext_with_insertable_encrypted.rs`:
- Around line 78-80: The closure is_encrypted currently uses the heuristic
!ix.data.suffix.as_bytes().is_empty() to detect encryption but lacks
explanation; add a concise inline comment next to the is_encrypted closure
(referencing is_encrypted, dlp::args::MaybeEncryptedInstruction, and
ix.data.suffix) that explains the convention: a non-empty suffix indicates the
presence of encryption metadata/marker bytes so the instruction should be
treated as encrypted.

In `@tests/test_lamports_settlement.rs`:
- Around line 239-252: Capture and record pre-finalize balances for the
validator wallet (the account queried via banks.get_account(authority.pubkey()
or its associated key) and for the validator vault (queried via
validator_fees_vault_pda_from_validator(&authority.pubkey())), then after
finalize assert exact expected deltas: verify delegated_account.lamports ==
new_delegated_account_lamports as before, assert validator_wallet_post ==
validator_wallet_pre - expected_delta, and assert validator_vault_post ==
validator_vault_pre (or unchanged aside from Rent::default().minimum_balance(0))
so the test checks that the extra lamports came from the validator wallet and
the vault did not absorb the increase. Ensure you reference and update the
existing variables/declarations around delegated_account, validator_vault,
validator_fees_vault_pda_from_validator, Rent, and banks.get_account calls when
adding the pre/post balance captures and exact assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 185e72f7-6d3f-4bbb-8a87-d669698cee48

📥 Commits

Reviewing files that changed from the base of the PR and between 95ca88d and 97432c4.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • tests/integration/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (84)
  • .coderabbit.yaml
  • .github/workflows/run-tests.yml
  • Cargo.toml
  • Makefile
  • dlp-api/Cargo.toml
  • dlp-api/src/cpi/delegate_with_actions.rs
  • dlp-api/src/cpi/mod.rs
  • dlp-api/src/decrypt.rs
  • dlp-api/src/encrypt.rs
  • dlp-api/src/encryption/mod.rs
  • dlp-api/src/instruction_builder/call_handler.rs
  • dlp-api/src/instruction_builder/call_handler_v2.rs
  • dlp-api/src/instruction_builder/close_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/close_validator_fees_vault.rs
  • dlp-api/src/instruction_builder/commit_diff.rs
  • dlp-api/src/instruction_builder/commit_diff_from_buffer.rs
  • dlp-api/src/instruction_builder/commit_finalize.rs
  • dlp-api/src/instruction_builder/commit_finalize_from_buffer.rs
  • dlp-api/src/instruction_builder/commit_state.rs
  • dlp-api/src/instruction_builder/commit_state_from_buffer.rs
  • dlp-api/src/instruction_builder/delegate.rs
  • dlp-api/src/instruction_builder/delegate_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/delegate_with_actions.rs
  • dlp-api/src/instruction_builder/finalize.rs
  • dlp-api/src/instruction_builder/init_protocol_fees_vault.rs
  • dlp-api/src/instruction_builder/init_validator_fees_vault.rs
  • dlp-api/src/instruction_builder/mod.rs
  • dlp-api/src/instruction_builder/protocol_claim_fees.rs
  • dlp-api/src/instruction_builder/top_up_ephemeral_balance.rs
  • dlp-api/src/instruction_builder/types/encryptable_types.rs
  • dlp-api/src/instruction_builder/types/mod.rs
  • dlp-api/src/instruction_builder/undelegate.rs
  • dlp-api/src/instruction_builder/undelegate_confined_account.rs
  • dlp-api/src/instruction_builder/validator_claim_fees.rs
  • dlp-api/src/instruction_builder/whitelist_validator_for_program.rs
  • dlp-api/src/lib.rs
  • src/args/delegate_with_actions.rs
  • src/args/mod.rs
  • src/compact/account_meta.rs
  • src/compact/instruction.rs
  • src/compact/mod.rs
  • src/consts.rs
  • src/diff/algorithm.rs
  • src/discriminator.rs
  • src/error.rs
  • src/lib.rs
  • src/processor/delegate_ephemeral_balance.rs
  • src/processor/fast/commit_state.rs
  • src/processor/fast/delegate.rs
  • src/processor/fast/delegate_with_actions.rs
  • src/processor/fast/finalize.rs
  • src/processor/fast/mod.rs
  • src/processor/fast/undelegate.rs
  • src/processor/fast/undelegate_confined_account.rs
  • src/processor/fast/utils/mod.rs
  • src/requires.rs
  • src/state/utils/try_from_bytes.rs
  • tests/test_call_handler.rs
  • tests/test_call_handler_v2.rs
  • tests/test_cleartext_with_insertable_encrypted.rs
  • tests/test_close_validator_fees_vault.rs
  • tests/test_commit_fees_on_undelegation.rs
  • tests/test_commit_finalize.rs
  • tests/test_commit_finalize_from_buffer.rs
  • tests/test_commit_on_curve.rs
  • tests/test_commit_state.rs
  • tests/test_commit_state_from_buffer.rs
  • tests/test_commit_state_with_program_config.rs
  • tests/test_commit_undelegate_zero_lamports_system_owned.rs
  • tests/test_delegate_on_curve.rs
  • tests/test_delegate_with_actions.rs
  • tests/test_delegation_confined_accounts.rs
  • tests/test_finalize.rs
  • tests/test_init_fees_vault.rs
  • tests/test_init_validator_fees_vault.rs
  • tests/test_lamports_settlement.rs
  • tests/test_protocol_claim_fees.rs
  • tests/test_top_up.rs
  • tests/test_undelegate.rs
  • tests/test_undelegate_confined_account.rs
  • tests/test_undelegate_on_curve.rs
  • tests/test_undelegate_without_commit.rs
  • tests/test_validator_claim_fees.rs
  • tests/test_whitelist_validator_for_program.rs
💤 Files with no reviewable changes (1)
  • src/processor/fast/utils/mod.rs

@snawaz snawaz merged commit abf3405 into main Mar 12, 2026
5 checks passed
@snawaz snawaz deleted the snawaz/delegate-with-actions branch March 12, 2026 14:20
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.

3 participants