feat(rpc): bind TEE attestations to versioned environment config hash#556
Merged
Conversation
Each `tee_generateQuote` response now carries a `katana_tee_config_hash` —
a Pedersen-array hash of `[KatanaTeeConfig1, chain_id, fee_token]` mirroring
the existing `compute_starknet_os_config_hash` shape — bound into both halves
of SEV-SNP `report_data`:
report_data[0..32] = Poseidon(KATANA_TEE_REPORT_VERSION,
KATANA_TEE_{APPCHAIN,SHARDING}_MODE,
...transition fields...,
katana_tee_config_hash)
report_data[32..64] = katana_tee_config_hash (raw)
The hash is precomputed at `TeeApi::new` from the chain spec — no caller-supplied
input. This rejects attestations from a different appchain configuration when
they reach the on-chain verifier (Piltover settlement), and removes the
caller-misconfiguration class where the wrong hash failed far from its cause.
Wire-format changes (v1-only — legacy zero-config reports are no longer emitted):
- `tee_generateQuote(prev, block, Option<Felt>)` -> `tee_generateQuote(prev, block)`
- `--katana-tee-config-hash` CLI flag removed from `katana rpc tee generate-quote`
- Legacy 7-field appchain / 8-field sharding Poseidon shapes deleted
- Test docs no longer reference downstream consumers (saya-tee, katana_tee_client)
Tests: 9 -> 10 (10/10 pass). New `generate_quote_precomputed_config_hash_binding`
asserts the configured hash appears in both halves of `report_data` and round-trips
through the response.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merged
4 tasks
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.30.
| Benchmark suite | Current: 0ba7eda | Previous: 4c7cabd | Ratio |
|---|---|---|---|
FinalityStatus/compress |
1 ns/iter (± 0) |
0 ns/iter (± 0) |
+∞ |
This comment was automatically generated by workflow using github-action-benchmark.
CC: @kariy
Codec benchmark diff vs
|
| Benchmark | Baseline (ns) | Current (ns) | Δ |
|---|---|---|---|
CompiledClass(fixture)/compress |
2558686 | 2673973 | +4.51% |
CompiledClass(fixture)/decompress |
2865553 | 2934578 | +2.41% |
ExecutionCheckpoint/compress |
35 | 32 | -8.57% |
ExecutionCheckpoint/decompress |
27 | 25 | -7.41% |
PruningCheckpoint/compress |
35 | 31 | -11.43% |
PruningCheckpoint/decompress |
27 | 25 | -7.41% |
VersionedHeader/compress |
662 | 641 | -3.17% |
VersionedHeader/decompress |
896 | 816 | -8.93% |
StoredBlockBodyIndices/compress |
82 | 79 | -3.66% |
StoredBlockBodyIndices/decompress |
40 | 36 | -10.00% |
StorageEntry/compress |
174 | 157 | -9.77% |
StorageEntry/decompress |
156 | 140 | -10.26% |
ContractNonceChange/compress |
178 | 156 | -12.36% |
ContractNonceChange/decompress |
260 | 232 | -10.77% |
ContractClassChange/compress |
220 | 210 | -4.55% |
ContractClassChange/decompress |
284 | 258 | -9.15% |
ContractStorageEntry/compress |
175 | 164 | -6.29% |
ContractStorageEntry/decompress |
345 | 310 | -10.14% |
GenericContractInfo/compress |
140 | 139 | -0.71% |
GenericContractInfo/decompress |
114 | 102 | -10.53% |
Felt/compress |
94 | 81 | -13.83% |
Felt/decompress |
63 | 54 | -14.29% |
BlockHash/compress |
93 | 82 | -11.83% |
BlockHash/decompress |
63 | 55 | -12.70% |
TxHash/compress |
93 | 80 | -13.98% |
TxHash/decompress |
63 | 54 | -14.29% |
ClassHash/compress |
95 | 83 | -12.63% |
ClassHash/decompress |
67 | 54 | -19.40% |
CompiledClassHash/compress |
93 | 81 | -12.90% |
CompiledClassHash/decompress |
63 | 55 | -12.70% |
BlockNumber/compress |
50 | 47 | -6.00% |
BlockNumber/decompress |
26 | 25 | -3.85% |
TxNumber/compress |
50 | 47 | -6.00% |
TxNumber/decompress |
26 | 28 | +7.69% |
FinalityStatus/compress |
0 | 1 | +Infinity% |
FinalityStatus/decompress |
12 | 12 | +0.00% |
TypedTransactionExecutionInfo/compress |
13884 | 17790 | +28.13% |
TypedTransactionExecutionInfo/decompress |
3711 | 3579 | -3.56% |
VersionedContractClass/compress |
360 | 374 | +3.89% |
VersionedContractClass/decompress |
833 | 778 | -6.60% |
MigratedCompiledClassHash/compress |
166 | 147 | -11.45% |
MigratedCompiledClassHash/decompress |
158 | 146 | -7.59% |
ContractInfoChangeList/compress |
1554 | 1569 | +0.97% |
ContractInfoChangeList/decompress |
2355 | 2203 | -6.45% |
BlockChangeList/compress |
681 | 662 | -2.79% |
BlockChangeList/decompress |
964 | 901 | -6.54% |
ReceiptEnvelope/compress |
26356 | 30903 | +17.25% |
ReceiptEnvelope/decompress |
6466 | 6030 | -6.74% |
TrieDatabaseValue/compress |
154 | 166 | +7.79% |
TrieDatabaseValue/decompress |
241 | 231 | -4.15% |
TrieHistoryEntry/compress |
286 | 293 | +2.45% |
TrieHistoryEntry/decompress |
289 | 249 | -13.84% |
|
The v1 schema bumped this function from 7 args to 8 (added `katana_tee_config_hash`), tripping `clippy::too_many_arguments`. The sharding sibling already carries the same allow attribute. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`TeeApi::new` now accepts the raw chain-spec inputs (`chain_id`, `fee_token_address`) and derives the versioned config hash internally, encapsulating the derivation behind a single constructor. This swaps the off-by-default-precomputed pattern for the right one: TeeApi owns its own environment binding. Tests pass raw inputs (the same values they used to feed into `compute_katana_tee_config_hash` themselves); callers only need to know the chain spec, not the hash construction. The wire format is unchanged — same Pedersen-array hash, same `KatanaTeeConfig1` tag, same dual-binding in both halves of `report_data`. Piltover's verification path is identical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the `(chain_id, fee_token_address)` pair on `TeeApi::new` with a single `chain_spec: &ChainSpec` argument. Field extraction (`chain_spec.id()` and `chain_spec.fee_contracts().strk`) and config-hash derivation happen inside the constructor. Why: the caller no longer needs to know which fields the v1 schema reads to compute the hash. If a future schema version reads more chain-spec fields, the signature stays the same. The wire format is unchanged. Tests build a minimal `ChainSpec` by cloning `dev::DEV` and overriding the two fields they care about. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #556 +/- ##
==========================================
- Coverage 73.32% 67.87% -5.45%
==========================================
Files 209 316 +107
Lines 23132 43511 +20379
==========================================
+ Hits 16961 29533 +12572
- Misses 6171 13978 +7807 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Picks up the merged PR #16 on cartridge-gg/piltover, which: - splits `ProgramInfo` into `StarknetOs` / `KatanaTee` enum variants - enforces a runtime variant invariant in `validate_input` so a TEE attestation cannot settle against a contract configured for SNOS, and vice versa The Cairo `katana_tee_config_hash` value Piltover stores for the TEE variant is the same `KatanaTeeConfig1`-tagged Pedersen-array hash this PR's `TeeApi` already produces, so the wire format stays unchanged. Compiled `Appchain` / `MockAmdTeeRegistry` artifacts get regenerated by `crates/contracts/build.rs`; no hard-coded class hash to update. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…llup`
Piltover's `ProgramInfo` is now an enum (StarknetOs vs KatanaTee variants),
and the on-chain `validate_input` panics on cross-mode submission. The init
flow needs to commit to the right variant at deploy time.
`deploy_settlement_contract` and `check_program_info` now take a `tee: bool`.
On `--tee`, init writes `ProgramInfo::KatanaTee(KatanaTeeProgramInfo {
katana_tee_config_hash })` using the same `compute_katana_tee_config_hash`
helper `TeeApi::new` calls, so the deployment-time hash and the runtime
attestation hash agree byte-for-byte. Otherwise, init writes the existing
`ProgramInfo::StarknetOs(...)` with the four SNOS hashes.
Verification adds two new errors: `InvalidKatanaTeeConfigHash` for hash
mismatch within a variant, and `InvalidProgramInfoVariant` for cross-mode
mismatch (StarknetOs config but operator passed `--tee`, or vice versa).
The default fee token address is lifted to a const so deployment and
verification share the same value.
Cargo.toml: repoints `piltover` from `kariy/piltover#feat/rpc0.9` to
`cartridge-gg/piltover#feat/tee-persistent` so the Rust types match the
submodule (single source of truth).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kariy
added a commit
that referenced
this pull request
Apr 28, 2026
## Summary Pre-installs `scarb 2.13.1` in `ghcr.io/dojoengine/katana-dev:latest` so the Piltover submodule build doesn't fall into a runtime install that OOMs the container's overlay disk. ## Why The Piltover submodule (`cartridge-gg/piltover#feat/tee-persistent`) pins `scarb 2.13.1` in its `.tool-versions`. The dev image only bakes in `2.8.2 / 2.11.4 / 2.12.2`, so `make install-scarb` falls into the runtime install path for `2.13.1`. The asdf-scarb plugin lies — every file copy fails with \"No space left on device\" but the script still prints \"installation was successful!\". Then when `cd piltover && asdf exec scarb build` runs, asdf can't find a real `scarb` binary, prints `No version is set for command scarb`, and `make contracts` fails. Repro from PR #556's most recent CI run: ``` Installing scarb 2.13.1... * Downloading scarb release 2.13.1... cp: error copying ... 'scarb-verify' ... No space left on device cp: error copying ... 'scarb' ... No space left on device cp: error copying ... 'SECURITY.md' ... No space left on device scarb 2.13.1 installation was successful! ← lying [later, from the piltover build dir] No version is set for command scarb Consider adding one of the following versions ... scarb 2.8.2 / 2.12.2 / 2.11.4 make: *** [Makefile:132: contracts] Error 1 ``` The Makefile already calls this out (lines 51-52): > Keeping the build path on 2.12.2 avoids installing an extra 2.13.1 toolchain in CI, which was exhausting disk space. ## Fix Add \`asdf install scarb 2.13.1\` to the existing \`RUN asdf plugin add scarb && ...\` block in \`.github/Dockerfile\`. Image grows by ~150-300MB; CI runtime install path for 2.13.1 becomes a cache hit (no disk pressure). ## Trigger This push touches \`.github/Dockerfile\`, so on merge \`.github/workflows/build-and-push-docker.yml\` rebuilds and re-tags \`ghcr.io/dojoengine/katana-dev:latest\`. PR #556 (the trigger for this fix) and any other branch that pulls in piltover submodule's new pin will go green once the new image is published. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ee variant dojoengine/saya@0072383 (`feat/tee` post-PR-#72) ships the new `ProgramInfo` enum (`StarknetOs` / `KatanaTee`) and plumbs `katana_tee_config_hash` through the TEE pipeline. The Saya `saya-ops setup-program` subcommand still emits only the `StarknetOs` variant, which would cause Piltover's runtime `validate_input` to panic with "mode: tee needs KatanaTee cfg" when saya-tee submits a `TeeInput` later in the test. Replace `saya.setup_program(...)` in `bootstrap_l2` with a direct multicall (`set_program_info(ProgramInfo::KatanaTee(...))` + `set_facts_registry`) using the prefunded L2 dev account. The `katana_tee_config_hash` is computed via the same `katana_rpc_api::tee::compute_katana_tee_config_hash` that the L3's `tee_generateQuote` uses, so the on-chain assertion that the attested hash equals the stored hash holds. CI bump: `.github/workflows/test.yml` checkout pin `5a3b8c9` → `0072383`. Source comments updated to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Saya renamed the `bin/ops` package from `saya-ops` to `ops` between revs `5a3b8c9` and `0072383`, so `cargo install --bin saya-ops` fails on the new rev with `no bin target named saya-ops, available: ops`. Switch the workflow's `cargo install` to `--bin ops` and update the test's PATH lookup in `resolve_saya_ops_bin()` to look for `ops`. The `SAYA_OPS_BIN` env var still works as an explicit override. The `saya-tee` binary (`bin/persistent-tee` package) keeps its name; that install step is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
saya@feat/tee@0072383 pins `cartridge-gg/katana-tee.git` via `ssh://git@github.com/...` URLs in `bin/persistent-tee/Cargo.toml`. CI doesn't have an SSH deploy key loaded for that repo, so `cargo install --path bin/persistent-tee` fails to clone the dep. `cartridge-gg/katana-tee` is public, so we can fetch it anonymously over HTTPS. Add `git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"` before the cargo install steps so cargo fetch resolves SSH URLs as HTTPS. (saya@main uses HTTPS URLs directly per dojoengine/saya#73; this workaround is only needed while the saya rev is on `feat/tee`.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dojoengine/saya@0a34e57 (`feat/tee` HEAD) switches the katana-tee deps in `bin/persistent-tee/Cargo.toml` from `ssh://git@github.com/...` to `https://github.com/...`. The CI runner can clone over HTTPS without needing an SSH deploy key, so the previous workaround (`git config --global url.https://...insteadOf ssh://...`) is no longer needed. Source comments updated to point at the new rev. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Saya's `main` already has both the ABI alignment (#73) and the ssh→https URL switch baked in, no need to track a `feat/tee` commit. Repoint `cargo install` and source comments at `5ff9948`, the current `main` HEAD. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`dojoengine/saya#74` updated `saya-tee --mock-prove` to emit the v1 report_data layout this PR enforces. Without that bump the e2e job reverts at fee estimation with `tee: config hash half mismatch`. 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
Each
tee_generateQuoteresponse now carries akatana_tee_config_hash— a Pedersen-array hash of[KatanaTeeConfig1, chain_id, fee_token]— bound into both halves of the SEV-SNPreport_data. The hash is precomputed atTeeApi::newfrom the chain spec, so the on-chain verifier (Piltover settlement) can reject attestations from a different appchain configuration. No caller input.The hash function mirrors the existing
compute_starknet_os_config_hash(bin/katana/src/cli/init/deployment.rs:407) — same Pedersen-array shape, same chain_id and STRK fee_token sources — so prior art carries through.Why now: the prior on-chain check bound only the transition fields. A node misconfigured for the wrong chain_id/fee_token could produce an attestation that Piltover would silently accept up until the state-transition mismatch surfaced elsewhere. v1 binds the environment into the report itself, so the rejection happens at verification time, not after.
Wire-format changes (v1-only — legacy zero-config reports are no longer emitted):
tee_generateQuote(prev, block, Option<Felt>)→tee_generateQuote(prev, block)--katana-tee-config-hashCLI flag removed fromkatana rpc tee generate-quotecompute_report_data_*Downstream coordination:
dojoengine/saya#74updatessaya-tee --mock-proveto emit the matching v1report_datalayout (10-field commitment in the first half,katana_tee_config_hashin the second half). Without that PR, everyupdate_statesettlement reverts at fee estimation withtee: config hash half mismatchand saya never submits a tx. The real-prove path is unaffected — the upstream Katana TEE node already produces v1 report_data this PR enforces.cartridge-gg/piltoveron thefeat/tee-persistentbranch (Workstream C), already merged astee-config-hash-v1.Test Coverage
Tests: 9 → 10 (+1 new). All 10 pass:
Pre-Landing Review
No critical or informational findings on this diff. Reviewed via
/plan-eng-reviewimmediately prior to commit, with five user-resolved decisions implemented verbatim:katana-tee::AMDTEERegistrystays generic)TeeApi::new(single Felt field on the struct)Specialist dispatch and adversarial review skipped: the eng review covered the same architectural surface with interactive Q&A; running them again would re-litigate settled decisions.
Plan Completion
Workstream A items (from PLAN.md):
Option<Felt>param: DONETeeQuoteResponse: DONEWorkstream B (
katana-teeCairo verifier): dropped —AMDTEERegistryis already generic, Piltover does report_data binding inline. Workstream C (piltoversettlement): separate repo, separate PR.Test plan
--features katana-rpc-server/tee-mockcargo check -p katana-sequencer-nodeclean (with and withouttee-mock).unwrap()panic on non-existentblockarg (crates/rpc/rpc-server/src/tee.rs:113) — captured as P1 in local working notes; will be a separate fix🤖 Generated with Claude Code