Flag unregistered programs in Solana intermediate output - #465
Flag unregistered programs in Solana intermediate output#465rituparna-mandal-anchor wants to merge 14 commits into
Conversation
Computed from idl::builtin_programs::is_trusted_program(program_key) rather than solana_parser's parsed_instruction.is_none(). The latter misses native programs decoded outside the IDL path entirely (System, Token, Token-2022) and this crate's own preset visualizers (Squads, Jupiter Swap, Drift, Kamino, etc.), both of which is_trusted_program already covers in one place. Bumps schema version since the borsh shape changed.
Adds true/false cases for SolanaIntermediateInstruction.is_unregistered, each asserting the flag survives both the SolanaInstruction -> SolanaIntermediateInstruction conversion and a full borsh round-trip.
Adds SimulatedInstruction to the proto (recursive: each entry carries its own inner_instructions), and a new SolanaMetadata.simulated_instructions field carrying the top-level list from a caller's pre-signing simulation. The converter attaches each top-level instruction's inner_instructions onto the matching statically-decoded entry by position, rather than replacing instructions wholesale. This preserves static decode's richer parsed_instruction_data for top-level entries while adding inner/CPI visibility that only simulation can provide -- the two decoders are complementary, not competing, so nothing existing is removed. Nesting (not flattening) matters for policy: a downstream consumer needs to tell "this unregistered call was requested directly by the user" (top-level) apart from "this unregistered call happened via CPI from an already-trusted program" (nested) -- those can warrant different treatment. is_unregistered stays a coarse, program-level hint (is_trusted_program). The authoritative allow/reject check is expected to run through the existing discriminator-level SCX allowlist mechanism downstream, using instruction_data_hex (preserved on every instruction regardless of source) -- not parsed_instruction_data, which simulated instructions never have.
Uses program IDs and instruction data from a real mainnet transaction where an unrecognized program invokes a trusted one via CPI, confirming is_unregistered evaluates each level independently.
|
Hi @rituparna-mandal-anchor, thank you for your contribution! It looks like this is your first time contributing. To get this PR merged, please review our Contributor License Agreement (CLA) here: https://ironcladapp.com/public-launch/6896309b0f158de8c7450de6 Once you have reviewed the agreement, a maintainer (@anchorageoss/maintainers) will need to approve your addition to our contributors list by commenting |
There was a problem hiding this comment.
Pull request overview
This PR extends the Solana intermediate-output pipeline to help downstream policy engines identify and reason about unknown programs, including CPI calls that are only observable via pre-signing simulation results.
Changes:
- Adds
is_unregisteredto Solana intermediate instructions, computed from the existing trusted-program set (independent of IDL matching). - Adds support for attaching caller-supplied simulated instruction trees (including nested CPI calls) to the intermediate output as
inner_instructions. - Bumps
SOLANA_INTERMEDIATE_SCHEMA_VERSIONfrom 1 → 2 and extends the proto + generated types to carry simulation instructions.
Reviewed changes
Copilot reviewed 6 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/generated/src/generated/parser.rs |
Adds simulated_instructions to SolanaMetadata and introduces the recursive SimulatedInstruction message type. |
src/codegen/src/main.rs |
Updates codegen attributes for serde + borsh for the new SimulatedInstruction type and field defaults. |
src/chain_parsers/visualsign-solana/tests/common/mod.rs |
Updates Solana test helpers to populate the new simulated_instructions field. |
src/chain_parsers/visualsign-solana/src/intermediate.rs |
Bumps intermediate schema version; adds is_unregistered + inner_instructions to the intermediate instruction model; adds conversions and tests. |
src/chain_parsers/visualsign-solana/src/core/visualsign.rs |
Plumbs simulated instructions from VisualSignOptions into intermediate output generation and attaches nested inner/CPI calls by top-level index. |
src/chain_parsers/visualsign-solana/src/cli_plugin.rs |
Ensures CLI-produced Solana metadata initializes simulated_instructions as empty. |
proto/parser/parser.proto |
Adds simulated_instructions to SolanaMetadata and defines the new SimulatedInstruction message. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| .field_attribute( | ||
| ".parser.SolanaMetadata.simulated_instructions", | ||
| SERDE_DEFAULT, | ||
| ) |
…a version 1 is_unregistered/inner_instructions are gated behind an opt-in flag with no live consumers yet, so they ship under the existing schema version rather than bumping it -- every decoder updates to the new shape before the flag is ever enabled. Also fixes a proto comment that inaccurately described simulateTransaction's real (flat, Index-keyed) response shape as nested per top-level instruction.
Replaces the nested per-top-level-instruction simulation attachment with a single flat simulated_instructions list, decoupled from static decode entirely. Removes the positional-alignment assumption between static decode and simulation results, and the untested length-mismatch degrade path that came with it.
| network_id: None, | ||
| idl: None, | ||
| idl_mappings: idl_mappings.into_iter().collect(), | ||
| simulate_transaction_result: None, |
There was a problem hiding this comment.
Nit: I think it should be simulated_ because it maps with rest of code better
There was a problem hiding this comment.
addressed, rename this var
| Self { | ||
| program_key: value.program_key.clone(), | ||
| instruction_data_hex: value.instruction_data_hex.clone(), | ||
| is_unregistered: !crate::idl::builtin_programs::is_trusted_program(&value.program_key), |
There was a problem hiding this comment.
We have to expand the concept of registered here because registered in our case doesn't just mean being in built in crates but also whether it's part of the signed IDLs in the request. I can't think of a better term to use here but essentially there's a materialized registry by this point to lookup in wherever that program came from.
There was a problem hiding this comment.
signed IDLs in the request
I thought IDLs were still being shipped as part of the OpenVSP binary for the time being. Has that already changed?
There was a problem hiding this comment.
We have had support for it for at least 8 months
There was a problem hiding this comment.
@prasincs @prasanna-anchorage Ok, I'm seeing this now -- thanks for linking the PR.
Basically the BE can send a map of IDLs (either signed or unsigned -- in this case we would want to always sign them, right?) and those get added to a registry. Then, when the parser is actually decoding, it first checks the IDLs stored as JSON files in the binary and if it doesn't find the program/instruction there, then it looks in the registry for the program/instruction.
It seems there is actually a third IDL source: solana_parser, which is included in the registry's implementation of has_idl() but is not a source in get_idl(), which seems to be a bug.
Based on this I have some questions:
- How is it safe to accept IDLs from the backend? Even if they are signed and we can verify in the TEE that the payload has not been tampered with (via the signed value), how is it safe to trust that the BE is sending the right IDL? This probably didn't matter when we were just using these for visualization -- but now that we are enforcing SCX policies on the output, this doesn't feel safe.
- Also, signing of the BE IDLs doesn't seem to be required currently so we don't even have that guarantee.
- Should the solana_parser IDLs be included in
get_idl()? - What is the north star for these sources? Will we want to maintain all three of these sources indefinitely or is there one source we are trying to converge on?
There was a problem hiding this comment.
replied out of band
shahan-khatchadourian-anchorage
left a comment
There was a problem hiding this comment.
Solid refactor — flattening in f2187da0 drops the positional-alignment assumption and reads much better than the nested version. Good test coverage for the size: native trust independent of IDL match, preset-only coverage via preset_program_ids(), a real on-chain router-CPI-ing-Jupiter case, borsh round-trip.
Ran a security pass too; nothing met the bar for a vulnerability. The provenance note below is legibility, not an exploit.
CI fails on f2187da0 — abi_metadata.rs:1029 missing simulate_transaction_result. It's in visualsign-ethereum's test target, so cargo build is clean and a Solana-crate test run never compiles it; --all-targets catches it.
is_unregistered only on the simulated list. c6ed0e8b had it on SolanaIntermediateInstruction ("Downstream policy engines (e.g. the HSM) use this to decide whether to reject a transaction containing it"); f2187da0 removed it. Deliberate, or collateral of the flattening? The static computation reads that instruction's own program_key and never depended on the positional alignment being removed.
As it stands the blob has two lists with opposite provenance — instructions[] from message_hex, simulated_instructions[] from caller metadata — and the only verdict is on the second. Consumers can't fill the gap: is_trusted_program's set (NATIVE_PROGRAM_NAMES + ProgramType::from_program_id + preset_program_ids()) lives in the binary and grows with every preset. Restoring looks like a revert of the c6ed0e8b field hunk plus the static-side variants of the three tests renamed to _via_simulation.
Schema version. c6ed0e8b bumped to 2, 7036a85f reverted. The appended length prefix ships on every emission, not just opted-in ones. A Rust mirror using borsh::from_slice or from_reader fails loudly — both reject trailing bytes — but a decoder calling deserialize directly, or a non-Rust mirror without a leftover-input check, reads a stale shape while its schema_version == 1 assert still passes. That second case is what the version field exists to prevent, and it's the one we can't verify from here. Cheaper to bump now than after anything adopts 1.
What "registered" resolves to. Not reopening the thread, just one constraint that isn't visible from the diff: extract_idl_mappings_with_signers validates a signature only when one is present and accepts unsigned IDLs (core/visualsign.rs:320, flagged in-tree as a known gap). So the materialized registry holds signed and unsigned alike, and a literal registry lookup would admit unsigned caller IDLs — not the ask upthread, which was scoped to signed. Matching formulation: built-ins + presets + caller IDLs with a verified, allowlisted signature. IdlRegistry::get_idl(&str) takes the base58 string directly, and the registry is already built at the call site (core/visualsign.rs:438) two lines above the conversion — so the gate is the decision, not the plumbing. Ideally one shared helper for both paths.
metadata_digest changes for every Solana request, include_intermediate_output or not — borsh writes a tag byte for the None. Does anything downstream recompute it for Solana, and does proto regeneration ship in the same rollout? Fails closed, and the Ethereum abi_mappings change set the precedent, so this is deploy ordering. Upside: the digest now covers simulate_transaction_result, so a consumer can bind what it supplied.
Smaller:
- Caller-asserted data has the same shape and apparent authority as enclave-derived fields, under a boolean that reads as an enclave verdict. Provenance is in prose at both layers but not in the encoding;
idl_source(intermediate.rs:121) is the existing idiom for tagging it. program_key/instruction_data_hexcopied verbatim. The static sibling ishex::encodeoutput fromsolana_parser, so one decode routine across both breaks on0xDEADBEEFor a non-base58 key.Pubkey::from_str/visualsign::encodings::decode_hexare the house pattern.intermediate.rs:292("computed the same way as the static-decode path") contradicts:65("the only placeis_unregisteredis computed").- The inline
serde(default)point still applies post-flattening:SimulateTransactionResult.instructionshas no default, so"simulateTransactionResult": {}fails through the gateway.
…tions WIP/PoC: parse jsonParsed inner instructions into intermediate output, tag CPIs against unregistered programs, and add Token-2022 TransferChecked support to the static decoder.
|
/approve-cla |
shahan-khatchadourian-anchorage
left a comment
There was a problem hiding this comment.
All six from last round addressed: CI green, registered_source on the static instructions, schema version 2 with the original wording restored, provenance in the encoding instead of a bool. Taking the raw RPC response instead of a caller-decoded list is a better shape — the caller's decode is out of the trust path. SolanaIdlParseError and the mainnet Jupiter fixture are both good.
Static decode output changed. extract_solana_intermediate_output now merges 19 preset IDLs into parse_transaction_with_idls with override_builtin: true (builtin_programs.rs:373); on origin/main that argument is caller configs only. For any transaction touching a preset program, simulation or not: idl_source flips "BuiltIn" -> "Custom" and idl_hash changes for the four IDs that are also solana_parser builtins (Drift, Meteora, Orca, Jupiter v6), and parsed_instruction_data goes None -> populated for the rest. The schema bump covers shape, not these values; the PR body's "byte-for-byte unchanged" no longer holds.
idl_source: "Custom" now covers both our presets and wallet-supplied IDLs — the distinction RegisteredSource draws two fields over. Add a Preset source string, or document that registered_source is the field to read.
Four silent paths to an empty simulation: base64 .ok()?, JSON .ok()?, construct_idl_records_map err -> Vec::new(), UiInstruction::Compiled -> continue. All byte-identical to "no simulation sent", no log. A caller sending the JSON-RPC envelope instead of the bare result, or non-jsonParsed encoding, gets nothing — and a consumer gating on "no unregistered programs" sees nothing to object to. The construct_idl_records_map case drops the Unregistered classifications too, which need no IDL at all. Warn at each; better, reject a present-but-unparseable field.
response.value.err dropped. A reverted simulation's partial trace attaches as complete; an unregistered call never reached looks like one that isn't there.
No bound on the payload. Caller IDL JSON is capped at 1 MiB in the same file; this isn't. It compounds: per-instruction IDL resolution means N inner instructions against a 438 KB Drift parse, and merge_preset_idl_configs clones ~2 MB on every intermediate request (was None in the common case). Memoize per program, cache the parsed map.
ENTRIES drifts from the derived set. preset_program_ids() enumerates available_visualizers(); ENTRIES is hand-maintained, and meteora_damm_v2.json is already missing from it — reports Preset with nothing to decode it. Worth a test that every preset dir with an IDL JSON appears in ENTRIES, plus the same step in solana-add-idl.
Smaller:
indexnever checked against the instruction count, though correlating back intoinstructions[]is its purpose.stack_heightdefaults to1(top-level) for entries that are all CPIs.0orOption<u32>reads as unknown.- Proto comment names
parse_transaction_with_idls; simulated instructions go throughparse_partially_decoded_instruction_idl. It's also silent on envelope-vs-resultand onjsonParsed, both of which fail silently when wrong. merge_preset_idl_configs's doc describes a caller override winning over a preset;from_idl_mappingsdrops trusted IDs, so it can't happen.
Carried over: metadata_digest still shifts for every Solana request.
|
/approve-cla |
|
ℹ️ @rituparna-mandal-anchor is already in the CLA signers list. |
|
@shahan-khatchadourian-anchorage re:
This one is stale as of a9e28a3. The structural point holds, with a different example. So the question the test would be pinning down isn't only "did someone forget an |
|
Reviewed a9e28a3. Direction is right — taking the raw RPC response instead of a caller-decoded list keeps the caller's decode out of the trust path, and Blocking1. Static decode output changed, and the PR body says it didn't.
This is content that gets signed. The schema bump covers shape, not values, so a consumer pinned to v2 sees no signal. "static decode's output is completely unaffected either way" in the PR body needs to go, and this needs to be a deliberate, called-out change rather than a side effect of wiring up the simulation path. Related: 2. Four silent paths to an empty simulation. All four produce output byte-identical to "no simulation was sent", none of them log:
A caller who sends the full JSON-RPC envelope instead of the bare The I'd reject a present-but-unparseable Also worth fixing
The fixture doesn't show the feature working. In
The Numbers on the cost.
CI: |
|
Correcting my own comment above: I framed So "registered but with no IDL to decode it" is not an edge case to close, it's the majority of the trusted set, and by design — System and SPL Token decode through That kills the "split One concrete consequence for the simulated path. A |
pepe-anchor
left a comment
There was a problem hiding this comment.
Reviewed with Claude on behalf of @pepe-anchor. 8 suggestion(s) of 10 drafted; 0 marked as blocking. Treat findings as suggestions, not directives.
Round 4 at b16cae66. That commit is a9e28a32 plus a rustfmt merge of main, so the code has not changed since round 3 and all eleven findings there are still open. I re-checked each against the tree rather than assuming. Not re-posting them inline, treat that review as live.
Two of them outrank most of what is below, so flagging them here: the override_builtin: true preset merge still changes idl_source and idl_hash on the static path for every transaction touching a preset (the PR body still says otherwise), and the four silent paths to an empty simulation still fail open on the exact signal this PR exists to provide.
New this round, mostly the rollout side, verified against the monorepo at a2517f7ed112:
- The schema bump has no tripwire. The HSM mirror is still at version 1, and both downstream tests are fixture- or pin-based, so neither fires on merge. Details on the constant.
- The
jsonParsedarm is wholly caller-authored. Line 462, and the one I would fix first. - The borsh round-trip test never encodes the new types, and two
RegisteredSourcevariants have no coverage at all.
Shape suggestion, take it or leave it. The proto field and the schema bump have different blast radii and could ship separately. The proto field is safe alone: ChainMetadata.metadata is a oneof, so Ethereum's bytes are byte-identical before and after, the Solana variant grows seven bytes but nothing constructs it, and there is no copy of parser.proto in the monorepo. The schema bump wants firmware first, ideally replacing the single constant with a supported set so a decoder tolerates N and N+1.
pepe-anchor
left a comment
There was a problem hiding this comment.
Inline notes for the round-4 review above.
| match parsed { | ||
| UiParsedInstruction::PartiallyDecoded(decoded) => { | ||
| let accounts = decoded.accounts; | ||
| let instruction_data_hex = bs58::decode(&decoded.data) |
There was a problem hiding this comment.
bs58::decode(...).unwrap_or_default() turns a decode failure into an empty instruction_data_hex.
When the program has an IdlRecord, the sibling call records DiscriminatorNotFound("instruction data is not valid base58") at :522, so the failure is visible. When it does not, parse_partially_decoded_instruction_idl early-returns (None, None) at :507 before the data is ever looked at, and the instruction lands with empty data and no error. That covers every Native and Unregistered program, since neither ships an IDL, so it is the common case rather than an edge one.
Extending Prasanna's bullet on this line rather than raising it fresh. Either record the failure here or keep the raw base58 string.
Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.
| idl_parse_error, | ||
| }); | ||
| } | ||
| UiParsedInstruction::Parsed(rpc_parsed) => { |
There was a problem hiding this comment.
This arm keeps only the caller's program and parsed JSON. accounts is Vec::new(), instruction_data_hex is String::new(), parsed_instruction_data is None, so nothing survives that a consumer could check the claim against. The sibling PartiallyDecoded arm re-derives from the base58 data; this one derives nothing.
So any instruction the caller marks jsonParsed is wholly caller-authored, while registered_source still reports whatever program_id maps to. A caller can send {"program":"system","programId":"1111...","parsed":{"type":"transfer","info":{"lamports":1}}} and the blob shows a benign System transfer, registered_source: Native, no bytes to re-derive from. Keep accounts and the base58 data at minimum, or run this arm through the same IDL re-derivation and treat the RPC decode as a hint.
Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.
|
|
||
| #[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq)] | ||
| pub struct SolanaSimulatedInstruction { | ||
| pub index: u32, |
There was a problem hiding this comment.
index is the outer instruction's index, so every entry in a CPI group shares it. :424 sets outer_index once per inner_instructions entry and every instruction pushed under it copies that value. With no doc comment, a consumer keying a dedup or a lookup on it will silently collide entries. outer_instruction_index, plus a line saying it is a grouping key rather than a unique one, would fix it.
Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.
| pub accounts: Vec<String>, | ||
| pub instruction_data_hex: String, | ||
| pub registered_source: RegisteredSource, | ||
| pub parsed_instruction_data: Option<SolanaParsedInstructionDataIo>, |
There was a problem hiding this comment.
These two are always mutually exclusive in practice, but nothing in the type says so. A consumer that checks only parsed_instruction_data, the natural move since it matches the sibling SolanaIntermediateInstruction, silently misses every RPC-recognised instruction. One enum instead of two Options makes that a compile error. Low priority though: out-of-tree mirrors have to track this shape, so a doc comment may be the better trade.
Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.
| /// set of program IDs the caller supplied a custom IDL for (the keys of | ||
| /// `IdlRegistry::get_all_configs()`), used only to detect `CallerSupplied`; | ||
| /// pass an empty map if unavailable. | ||
| pub fn registered_source( |
There was a problem hiding this comment.
ThirdParty and CallerSupplied have no test coverage. Each has exactly one construction site, :239 and :241, and neither appears in any test module. The tests only ever assert Native, Preset and Unregistered.
Those two are the variants that decide whether policy is looking at a program we compiled in or one the caller described to us. A case for each, a ProgramType-only program and one present only in idl_mappings, would also pin down Prasanna's question about what Preset asserts.
Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.
| pub spl_transfers: Vec<SplTransfer>, | ||
| pub recent_blockhash: String, | ||
| pub address_table_lookups: Vec<SolanaAddressTableLookup>, | ||
| pub simulated_instructions: Vec<SolanaSimulatedInstruction>, |
There was a problem hiding this comment.
The bump to schema 2 exists to protect the new bytes, and two of the new types never see borsh in a test.
SolanaRpcParsedInstructionDataIo has no test occurrence at all, it appears only at :105, :168 and :478. SolanaIdlParseError is asserted on in the Jupiter test, but that path never encodes, and the two populated round-trips in core/visualsign.rs reach RegisteredSource without ever reaching an error variant. intermediate_output_round_trip_is_deterministic builds instructions: vec![], so it encodes neither.
SolanaIdlParseError is the half worth covering. It is a borsh enum with named-struct and tuple variants, materially more work for a hand-written mirror decoder than the flat structs around it, and DiscriminatorNotFound is the only variant any test touches. Worth extending the round-trip test to a populated instruction and simulated instruction carrying a non-trivial error variant.
Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.
| network_id: None, | ||
| idl: None, | ||
| idl_mappings: idl_mappings.into_iter().collect(), | ||
| simulated_transaction_result: None, |
There was a problem hiding this comment.
SolanaArgs has only idl_json_mappings and create_chain_metadata hardcodes simulated_transaction_result: None, so the CLI cannot exercise the feature while the gRPC path can.
That bites twice: parser_cli decode is the documented way to inspect a transaction locally, and it is the practical way to produce a fixture for an out-of-tree decoder. As it stands there is no way to emit a v2 blob with populated simulated instructions without going through the service. A --simulated-tx-result flag threaded like idl_json_mappings would do it.
Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.
| /// to the shape below. Mirrored decoders assert this value, so a bump makes a | ||
| /// schema drift fail loudly instead of silently misparsing. | ||
| pub const SOLANA_INTERMEDIATE_SCHEMA_VERSION: u16 = 1; | ||
| pub const SOLANA_INTERMEDIATE_SCHEMA_VERSION: u16 = 2; |
There was a problem hiding this comment.
Two things on the bump itself.
The doc comment says "Mirrored decoders assert this value" without saying where any of them live, and CONTRIBUTING does not either. Naming them, or at least stating that out-of-tree hand-written decoders exist, is the cheapest fix here. Right now nothing in this repo tells a contributor bumping this constant who else has to move.
Second, the guarantee this comment claims does not hold for a decoder that reads the whole body before it looks at the version. Borsh is positional, so a real v2 blob misaligns on the extra per-instruction fields and dies on a short read or a trailing-bytes check well before any schema_version comparison runs. It fails closed either way, so this is diagnosability rather than safety, but the operator gets an alignment error instead of "schema_version 2 is not supported". A mirror only gets the clean error if it reads the u16 first and dispatches on it, which is also what any decoder accepting more than one version needs. Worth stating that requirement next to the constant: the field being first is what makes it possible, and nothing currently says why that ordering matters.
Authored by Claude on behalf of @pepe-anchor. Please flag any inaccuracies, I'm not always right.
|
Drive-by perf note from benchmarking the intermediate path on #472 — not a blocker, but it lands on a hot path.
For scale: on #472 I benchmarked
The bigger win is one variant over. pub enum CustomIdl {
/// A pre-parsed IDL struct (avoids re-parsing)
Parsed(Idl),
/// An IDL as a JSON string (will be parsed)
Json(String),
}
Killing the remaining clone entirely would need Happy to send a patch for the |
|
One more from re-reading the simulation path — I think this one is worth holding on, separate from the perf note above. Reverted simulations are consumed as if they succeeded
let response: Response<RpcSimulateTransactionResult> = serde_json::from_slice(raw_json).ok()?;
let inner_instructions = response.value.inner_instructions?;
Some(decode_inner_instructions(inner_instructions, idl_registry))
So a simulation that reverted contributes its partial CPI trace to That matters more here than it would elsewhere, because of what this field is for. Suggested shape: treat Two smaller cases with the same shapeBoth turn "we couldn't read this" into "there was nothing here":
For a field that feeds policy, "absent" and "unparseable" probably shouldn't be the same value. Worth at least a Happy to be wrong on the severity of the first one if the caller contract already guarantees a successful simulation — but I couldn't find that stated anywhere, and the proto comment on |
An unreadable simulated_transaction_result encoded the same as one that had no inner instructions, so a consumer gating on the trace passed on input we never read. - Add SolanaIntermediateOutput.simulation_error - Report invalid base64 and non-simulateTransaction JSON (incl. the JSON-RPC envelope) instead of dropping to None - Drop a reverted simulation's partial trace, tag value.err - Tag compiled inner instructions instead of skipping them - Tag unusable caller IDLs - Document that RegisteredSource says nothing about decodability - Fix create_chain_metadata test call sites missed in f0d4c51 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
unaffected either way.
Why
Policy engines need to catch unrecognized programs, including ones that only become visible through pre-signing simulation.
Test plan
binary encoding intact