Emit Solana intermediate output for Turnkey policy engine#282
Draft
prasanna-anchorage wants to merge 3 commits into
Draft
Emit Solana intermediate output for Turnkey policy engine#282prasanna-anchorage wants to merge 3 commits into
prasanna-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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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