Skip to content

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

@mihailShumilov

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions