Skip to content

feat(rpc): bind TEE attestations to versioned environment config hash#556

Merged
kariy merged 18 commits into
mainfrom
tee-challenge-onchain
May 4, 2026
Merged

feat(rpc): bind TEE attestations to versioned environment config hash#556
kariy merged 18 commits into
mainfrom
tee-challenge-onchain

Conversation

@kariy
Copy link
Copy Markdown
Member

@kariy kariy commented Apr 27, 2026

Summary

Each tee_generateQuote response now carries a katana_tee_config_hash — a Pedersen-array hash of [KatanaTeeConfig1, chain_id, fee_token] — bound into both halves of the SEV-SNP report_data. The hash is precomputed at TeeApi::new from the chain spec, so the on-chain verifier (Piltover settlement) can reject attestations from a different appchain configuration. No caller input.

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, decodable as felt252)

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-hash CLI flag removed from katana rpc tee generate-quote
  • Legacy 7-field appchain / 8-field sharding Poseidon shapes deleted in compute_report_data_*
  • Test docs no longer reference downstream consumers (saya-tee, katana_tee_client) — replaced with behavioral contract statements

Downstream coordination:

  • dojoengine/saya#74 updates saya-tee --mock-prove to emit the matching v1 report_data layout (10-field commitment in the first half, katana_tee_config_hash in the second half). Without that PR, every update_state settlement reverts at fee estimation with tee: config hash half mismatch and saya never submits a tx. The real-prove path is unaffected — the upstream Katana TEE node already produces v1 report_data this PR enforces.
  • Piltover's matching v1 inline verification lives in cartridge-gg/piltover on the feat/tee-persistent branch (Workstream C), already merged as tee-config-hash-v1.

Test Coverage

[+] crates/rpc/rpc-server/src/tee.rs (TeeApi::generate_quote)
    │
    ├── prev_block None → Felt::MAX sentinel       [★★★ TESTED] genesis_appchain
    ├── prev_block Some(num) Ok(Some)              [★★★ TESTED] l2_to_l1_aggregation
    ├── prev_block Some(num) Ok(None)              [★★★ TESTED] missing_prev_block_errors
    ├── fork_block_number Some → sharding v1       [★★★ TESTED] fork_sharding_mode + sharding_non_zero_config_hash
    ├── fork_block_number None → appchain v1       [★★★ TESTED] genesis_appchain + appchain_non_zero_config_hash
    ├── L2→L1 multi-block message aggregation      [★★★ TESTED] l2_to_l1_aggregation
    ├── L1→L2 receipt+tx zip                       [★★★ TESTED] l1_to_l2_aggregation
    ├── precomputed hash → both halves of report   [★★★ TESTED] precomputed_config_hash_binding (NEW)
    └── prev_block_number serde: None ↔ Felt::MAX  [★★★ TESTED] prev_block_number_wire_format

Coverage: ~80% of changed code paths. Gaps deferred:
  - Provider error paths beyond block_hash_by_num (low risk, mock can't easily inject)
  - TEE provider failure (MockProvider has no failure mode; out of scope)
  - Latent .unwrap() panic at tee.rs:113 (pre-existing, captured in TODOS.md as P1)
  - Cross-language parity fixture (Phase 0.3 deliverable, separate PR)

Tests: 9 → 10 (+1 new). All 10 pass:

cargo nextest run -p katana-rpc-server --features katana-rpc-server/tee-mock --test tee
Summary: 10 tests run: 10 passed, 0 skipped (in 0.124s)

Pre-Landing Review

No critical or informational findings on this diff. Reviewed via /plan-eng-review immediately prior to commit, with five user-resolved decisions implemented verbatim:

  1. Compute config hash internally from chain spec (no caller-supplied parameter)
  2. Pure verifier role (Piltover does inline binding; katana-tee::AMDTEERegistry stays generic)
  3. v1-only — drop legacy zero-config code path
  4. Precompute at TeeApi::new (single Felt field on the struct)
  5. Delete legacy report-data branches in Rust

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):

  • A.1 drop Option<Felt> param: DONE
  • A.2 v1 report-data with mode + config hash: DONE
  • A.3 config hash in second 32 bytes: DONE
  • A.4 return hash in TeeQuoteResponse: DONE
  • A.5 v1 test vectors + precomputed-binding: PARTIAL (binding test landed; provider-error/TEE-failure paths and cross-language parity fixture deferred to follow-up)
  • A.6 delete legacy branches + migrate tests + drop two-param test: DONE
  • A.7 test docs cleanup (no downstream consumer references): DONE

Workstream B (katana-tee Cairo verifier): dropped — AMDTEERegistry is already generic, Piltover does report_data binding inline. Workstream C (piltover settlement): separate repo, separate PR.

Test plan

  • All TEE integration tests pass with --features katana-rpc-server/tee-mock
  • cargo check -p katana-sequencer-node clean (with and without tee-mock)
  • Cross-language parity fixture (Phase 0.3) — follow-up PR
  • Pre-existing .unwrap() panic on non-existent block arg (crates/rpc/rpc-server/src/tee.rs:113) — captured as P1 in local working notes; will be a separate fix

🤖 Generated with Claude Code

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>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 27, 2026

Codec benchmark diff vs main

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%

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 27, 2026

Runner: AMD EPYC 7763 64-Core Processor (4 cores) · 15Gi RAM

kariy and others added 4 commits April 27, 2026 22:45
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
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 26.08696% with 102 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.87%. Comparing base (9bde0ae) to head (0ba7eda).
⚠️ Report is 395 commits behind head on main.

Files with missing lines Patch % Lines
bin/katana/src/cli/init/deployment.rs 0.00% 67 Missing ⚠️
bin/katana/src/cli/init/prompt.rs 0.00% 27 Missing ⚠️
crates/node/sequencer/src/lib.rs 0.00% 6 Missing ⚠️
bin/katana/src/cli/init/mod.rs 0.00% 2 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

kariy and others added 2 commits April 28, 2026 14:19
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>
kariy and others added 10 commits April 29, 2026 00:20
…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>
@kariy kariy merged commit 9f4f721 into main May 4, 2026
19 of 21 checks passed
@kariy kariy deleted the tee-challenge-onchain branch May 4, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant