Emit Solana intermediate output for Turnkey policy engine#282
Emit Solana intermediate output for Turnkey policy engine#282prasanna-anchorage wants to merge 3 commits into
Conversation
Adds an `intermediate_output: optional bytes` field to ParsedTransactionPayload that carries a borsh-serialized chain-specific structured view of the transaction, intended for downstream policy engines (Turnkey's Solana policy engine in particular). For Solana, the schema (visualsign_solana::intermediate::SolanaIntermediateOutput) mirrors solana_parser::SolanaMetadata, scoped to the fields Turnkey's policy DSL evaluates against (account_keys, program_keys, instructions, transfers, spl_transfers, recent_blockhash, address_table_lookups, plus parsed_instruction_data per instruction). Maps use BTreeMap and program_call_args is rendered as canonical alphabetized JSON, so the borsh blob is byte-deterministic — the request signature now covers it without further code. Other chains return None for intermediate_output; the bytes field is covered by the existing borsh-based signature path in routes/parse.rs. Adds a `--with-intermediate` CLI flag that decodes and pretty-prints the borsh blob (Solana-only for now). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds cel-interpreter (Google's Common Expression Language, the spec Turnkey says their policy engine is based on) as a dev-dep on visualsign-solana and a regular dep on parser_cli. Tests live in src/chain_parsers/visualsign-solana/tests/policy_examples.rs and exercise 6 representative policy patterns from Turnkey's Solana policy-engine announcement against a known Jupiter-swap fixture: designated sender, single-recipient + count, blocked address, program-set allowlist, no address-table-lookups, and IDL-aware instruction-name check. The CLI gains a `--policy <EXPR>` flag (repeatable). It decodes the borsh intermediate output, binds it as `solana` in a CEL context, and prints PASS/DENY per expression. Process exits non-zero if any policy denies, suitable for CI gating. Note: Turnkey's docs surface `.any(t, p)` / `.count` aliases that canonical CEL doesn't ship with — use `.exists(t, p)` / `size(...)` instead. Same semantics; see the tests/CLI help for the mapping. These tests assert the schema is *expressive enough* to encode Turnkey-style policies. Faithful pass/deny still requires Turnkey-side evaluation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Combining rules" section to --policy CLI help and to the policy_examples.rs module docs, plus a callout that engine-level structure (effect: ALLOW/DENY, consensus formulas) lives above CEL and is out of scope for this PoC. Surfaces: - single-expression operators (&&, ||, !, ternary, .exists/.all/size) - multi-flag CLI semantics: implicit AND, exit 2 on any DENY - the gap between CEL conditions and full Turnkey-style policy engines No code changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Confidential Transfer (Token-2022) support — proposed intermediate fields + plan (PRS-252)Flagging this while the Context. PRS-252 builds Solana Token-2022 Confidential Transfer signing (CVM-assisted; the outbound transfer + a confidential→public withdraw are now validated end-to-end on regnet + live devnet). We want the HSM-signed CT path to ride this Scope = a parser extension, not new crypto. The ZK proofs (equality / range / grouped-validity) are produced off-VSP (HSM Proposed CT fields in the intermediate schema (Token-2022 ConfidentialTransfer):
First pass will be manually crafted. We intend a hand-written CT instruction parser/preset — explicit decode of the ConfidentialTransfer instruction discriminators + their account/data layout — rather than relying on generic Next step: a manually-crafted |
Summary
optional bytes intermediate_output = 5toParsedTransactionPayload. For Solana, it carries a borsh-serialized, deterministic mirror ofsolana_parser::SolanaMetadatashaped to thesolana.tx.*attributes Turnkey's Solana policy engine evaluates. Empty for other chains.VisualSignConvertertrait to returnConversionResult { payload, intermediate_output: Option<Vec<u8>> }. The existing borsh-basedParsedTransactionPayloadsignature now covers the new field automatically — wallets can trust it the same way they trustparsed_payload.parser_cli --with-intermediateto decode and pretty-print the borsh blob.parser_cli --policy <EXPR>to evaluate Google CEL policy expressions against the parsed intermediate output and print PASS/DENY (process exits non-zero on any DENY, so it composes with CI).visualsign_solana::intermediate(apub mod) so wallet code canborsh::from_slicedirectly.This is a draft PoC — comments welcome on the schema shape and the trait change before we polish.
Why borsh + bytes (vs proto-typed nested messages)?
program_call_args_json), which keeps the existingborsh::to_vec(&payload)digest stable.Schema (
SolanaIntermediateOutput)Mirrors
solana_parser::SolanaMetadataminussignatures(unsigned tx). Per-instruction:Plus top-level
account_keys, program_keys, transfers, spl_transfers, recent_blockhash, address_table_lookups. Field names match Turnkey's documentedsolana.tx.*attributes.CLI invocation (PoC)
Pretty-print the intermediate output:
Trimmed sample (Jupiter swap fixture from the existing unit test):
CEL policy evaluation (
--policy)The
--policy <EXPR>flag (repeatable) compiles CEL against the deserialized intermediate output:Process exits with code 2 if any policy denies, so it composes with CI.
The implementation uses
cel-interpreter, a Rust port of Google's CEL spec — the same DSL Turnkey says their policy engine is based on. Caveats:xs.any(t, p)xs.exists(t, p)xs.countsize(xs)Same semantics, slightly different surface. We could register a thin
anyalias on the evaluator if byte-identical Turnkey syntax matters; left out here for simplicity.This is a schema-expressiveness check, not a Turnkey-faithful simulator. Real pass/deny still requires the server-side engine.
Sample policy expressions
Eight patterns adapted from the Turnkey announcement:
Six of these are exercised in
tests/policy_examples.rsagainst a real fixture (Jupiter swap), demonstrating the schema is expressive enough.Determinism
The
intermediate_outputbytes are part ofborsh::to_vec(&ParsedTransactionPayload), so the existing signature covers them. To keep the digest stable for identical inputs:Vecfields preserve transaction order.BTreeMap(deterministic borsh).program_call_args_jsonis built from aBTreeMap<String, &Value>thenserde_json::to_string, producing alphabetized JSON.A determinism unit test in
intermediate.rsround-trips borsh and asserts byte-identical output.Test plan
make -C src lint— clippy cleanmake -C src test— all unit + integration + new policy tests passsrc/integration/tests/parser.rsthat decodesparsed_transaction.payload.intermediate_outputand asserts shape (follow-up)intermediate_output.is_none()(follow-up)Follow-ups
solana_parserfromanchorageoss/a0c554dtotkhq/main(e93f042) oncesolana-parser-fuzz-corerebases onto main; current pin already exposes the API we need.any/countaliases on the CEL evaluator for byte-identical Turnkey syntax.🤖 Generated with Claude Code