Skip to content

encrypt-anchor: invoke_execute_graph drops outer-tx signer flags + omits system_program (CREATE-mode breakage) + undocumented error 0x14 #7

Description

@mihailShumilov

Two encrypt-anchor invoke_execute_graph bugs blocking CREATE-mode + an undocumented on-chain error

Hi! While integrating Encrypt + Anchor for a 6-input/3-output computation graph (an FHE order-matching comparator), I ran into three issues at the rev dadfff8c of encrypt-anchor. Patches that move our flow forward are linked at the bottom; happy to PR if helpful.

Setup

  • obsidian-core Anchor program calls EncryptContext::match_orders_graph(...) (generated by #[encrypt_fn]).
  • 6 input ciphertext accounts (gRPC-createInput'd), 3 output ciphertext accounts.
  • Encrypt program at 4ebfzWdKnrnGseuQpezXdG8yCdHqwQ1SSBHD3bWArND8 on Solana devnet.
  • Toolchain: anchor-cli 1.0.2 / Rust 1.94 / encrypt-anchor + encrypt-solana-dsl pinned at rev dadfff8c.

Bug 1 — invoke_execute_graph demotes outer-tx signer (and writable) flags

File: chains/solana/program-sdk/anchor/src/lib.rs

Inside invoke_execute_graph, execute_graph, and execute_registered_graph, every encrypt_execute_account is pushed with hard-coded AccountMeta::new(acct.key(), false):

for acct in encrypt_execute_accounts {
    accounts.push(AccountMeta::new(acct.key(), false));
}

This always emits is_signer = false and is_writable = true regardless of the outer-tx state.

Symptom (CREATE mode, fresh keypair output cts): the outer tx passes the 3 output cts with isSigner = true so the inner system_program::create_account can authorise their allocation. After the demotion at depth 2, that inner CPI fails:

Program 4ebfzW...ND8 invoke [2]
<output_pubkey> writable privilege escalated
Program 4ebfzW...ND8 failed: Cross-program invocation with unauthorized signer or writable account

(or signer privilege escalated depending on the variant).

Fix: preserve the AccountInfo's is_signer and is_writable flags:

fn account_meta_for(acct: &AccountInfo) -> AccountMeta {
    if acct.is_writable {
        AccountMeta::new(acct.key(), acct.is_signer)
    } else {
        AccountMeta::new_readonly(acct.key(), acct.is_signer)
    }
}

This works for all three call sites and matches the upstream voting-example behaviour (whose outputs overlap with inputs and are thus never signers).

Bug 2 — invoke_execute_graph omits system_program from its account list

File: same.

The fixed prefix in invoke_execute_graph includes config / deposit / caller_program / cpi_authority / network_encryption_key / payer / event_authority / encrypt_program — but not system_program. (It's in EncryptContext and IS included by register_graph / execute_registered_graph per their fixed prefixes.)

Symptom (after Bug 1 patched, CREATE mode): Encrypt's handler runs and tries to invoke system_program::create_account to allocate a fresh output ct. The system_program isn't in the CPI's account_infos and the runtime rejects:

Program 4ebfzW...ND8 invoke [2]
Unknown program 11111111111111111111111111111111
Program 4ebfzW...ND8 failed: An account required by the instruction is missing

Fix: add AccountMeta::new_readonly(self.system_program.key(), false) to the fixed prefix and self.system_program.clone() to account_infos. The voting example doesn't trip this because its UPDATE-mode outputs already exist; CREATE mode hits it immediately.

Bug 3 — Deployed Encrypt program returns custom error 0x14 (=20) not in IDL

File: chains/solana/idl/encrypt_program.json documents errors 0–17. The deployed program at 4ebfzWdKnrnGseuQpezXdG8yCdHqwQ1SSBHD3bWArND8 returns custom program error: 0x14 for our compiled match_orders_graph graph (after Bugs 1 + 2 are patched).

Symptom: the CPI dispatches successfully past the runtime checks; Encrypt enters its handler, consumes 1978 compute units, and rejects with no msg!() diagnostic:

Program 4ebfzW...ND8 invoke [2]
Program 4ebfzW...ND8 consumed 1978 of 169534 compute units
Program 4ebfzW...ND8 failed: custom program error: 0x14

The fast-fail (1978 CUs) suggests an early validation path. The deployed program's source isn't in this checkout, so I can't verify what error variant 20 represents. Plausible candidates given the 0–17 list: graph hash registration drift (we use the inline execute_graph discriminator = 4u8, not the registered-graph variant), input-ct shape check, or an undocumented permission check past 17.

Ask: could the IDL be regenerated against the deployed-program rev so code: 20 shows up with a name + message? Even better, a msg!() log at each early validation point would be hugely diagnostic.

Reproducer

obsidian-core is open-source and the failing path is exercised by:

git clone https://github.com/mihailShumilov/obsidian-desk
cd obsidian-desk
# Cargo.toml currently points obsidian-core at a path-vendored copy of
# encrypt-anchor with the Bug 1 + Bug 2 patches above. Tests against the
# upstream rev are easy to flip back to via the commented git dep.
anchor build --no-idl --ignore-keys
ANCHOR_PROVIDER_URL=<your-devnet-rpc> ANCHOR_WALLET=~/.config/solana/id.json \
  pnpm exec tsx keeper/scripts/devnet-bootstrap.ts
ANCHOR_PROVIDER_URL=<your-devnet-rpc> \
  pnpm exec tsx keeper/scripts/match-pair.ts <market> <orderA> <orderB>

The patches that move us past Bugs 1 + 2 are at:

  • crates/encrypt-anchor-vendor/src/lib.rs — the targeted diff vs your dadfff8c source. ~12 lines of net change, comments explaining each block. Happy to PR back if useful.

Thanks for the great DSL — the rest of the integration was straightforward!

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions