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!
Two
encrypt-anchorinvoke_execute_graphbugs blocking CREATE-mode + an undocumented on-chain errorHi! 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
dadfff8cofencrypt-anchor. Patches that move our flow forward are linked at the bottom; happy to PR if helpful.Setup
obsidian-coreAnchor program callsEncryptContext::match_orders_graph(...)(generated by#[encrypt_fn]).createInput'd), 3 output ciphertext accounts.4ebfzWdKnrnGseuQpezXdG8yCdHqwQ1SSBHD3bWArND8on Solana devnet.dadfff8c.Bug 1 —
invoke_execute_graphdemotes outer-tx signer (and writable) flagsFile:
chains/solana/program-sdk/anchor/src/lib.rsInside
invoke_execute_graph,execute_graph, andexecute_registered_graph, everyencrypt_execute_accountis pushed with hard-codedAccountMeta::new(acct.key(), false):This always emits
is_signer = falseandis_writable = trueregardless of the outer-tx state.Symptom (CREATE mode, fresh keypair output cts): the outer tx passes the 3 output cts with
isSigner = trueso the innersystem_program::create_accountcan authorise their allocation. After the demotion at depth 2, that inner CPI fails:(or
signer privilege escalateddepending on the variant).Fix: preserve the AccountInfo's
is_signerandis_writableflags: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_graphomitssystem_programfrom its account listFile: same.
The fixed prefix in
invoke_execute_graphincludes config / deposit / caller_program / cpi_authority / network_encryption_key / payer / event_authority / encrypt_program — but notsystem_program. (It's inEncryptContextand IS included byregister_graph/execute_registered_graphper their fixed prefixes.)Symptom (after Bug 1 patched, CREATE mode): Encrypt's handler runs and tries to invoke
system_program::create_accountto allocate a fresh output ct. The system_program isn't in the CPI'saccount_infosand the runtime rejects:Fix: add
AccountMeta::new_readonly(self.system_program.key(), false)to the fixed prefix andself.system_program.clone()toaccount_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.jsondocuments errors 0–17. The deployed program at4ebfzWdKnrnGseuQpezXdG8yCdHqwQ1SSBHD3bWArND8returnscustom program error: 0x14for our compiledmatch_orders_graphgraph (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: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_graphdiscriminator = 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: 20shows up with a name + message? Even better, amsg!()log at each early validation point would be hugely diagnostic.Reproducer
obsidian-coreis open-source and the failing path is exercised by:The patches that move us past Bugs 1 + 2 are at:
crates/encrypt-anchor-vendor/src/lib.rs— the targeted diff vs yourdadfff8csource. ~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!