Skip to content

test(contract): cover the Verified attestation path cross-contract in sandbox - #4110

Open
pbeza wants to merge 36 commits into
mainfrom
4084-verified-attestation-cross-contract
Open

test(contract): cover the Verified attestation path cross-contract in sandbox#4110
pbeza wants to merge 36 commits into
mainfrom
4084-verified-attestation-cross-contract

Conversation

@pbeza

@pbeza pbeza commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Stacked on #4109. Drives the async submit_participant_info flow in sandbox against a real deployed tee-verifier, covering all three outcomes: Rejected, no-verdict (verifier unreachable), and Verified.

Closes #4084

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

This PR includes changes to source code files in crates/tee-verifier/ and crates/tee-verifier-interface/. The test type should only be used when changing test files exclusively. Since source code is being modified, feat or refactor would be more appropriate.

Suggested title: feat(tee-verifier): add Verified attestation path cross-contract support

This comment was marked as outdated.

@claude

This comment was marked as outdated.

@pbeza
pbeza force-pushed the 4084-verified-attestation-cross-contract branch from b5e55c3 to faea559 Compare August 10, 2026 13:56
pbeza added 12 commits August 11, 2026 13:45
…tion

The MPC node generates its NEAR signer key inside the enclave and writes it
only to the CVM's encrypted disk. Collecting test assets needs that key
alongside the quote it is bound to, and an app-compose pre-launch script is
the only way in: the dev image ships no sshd and the guest agent serves only
node info and container logs.

vmm-cli already supports --prelaunch-script; this just plumbs an optional
PRELAUNCH_SCRIPT through. Attestation verification rejects any app-compose
carrying a script, so a CVM deployed this way cannot join a network - the
deploy guide, default.env and the script itself now say so loudly.
Collecting the attestation fixtures requires a pre-launch script to export
the node's in-enclave signer key, which makes the resulting app-compose one
that production verification rejects as arbitrary root code. The new
allow-pre-launch-script feature relaxes exactly that field for test builds;
init_script and bash_script stay rejected, and the production wasm never
enables it.

The check takes the policy as an argument so the strict behaviour keeps its
unit test in test builds too, and a further test pins the compiled-in policy
at the call site.
…ner key

Regenerated from a fresh localnet CVM on a TDX host, keeping the node's NEAR
signer key this time so tests can sign submit_participant_info as the fixture
node. The quote's report_data binds that key and the contract reads it from
the transaction signer, so without it the Verified store path is unreachable
(#3787).

create-assets.sh had been producing collateral our own parser cannot read
since the tee-verifier-interface refactor: nodes serialize the DER and
signature fields as JSON byte arrays while collateral_from_json expects hex
strings. It now hex-encodes those fields, and strips the NUL that terminates
the PEM chains so the fixture stays valid PEM. The regeneration reminder is
unconditional, since the committed key is normally present but stale.

.editorconfig no longer asserts the borsh fixture's final byte: it is
whatever the encoded collateral ends with.
… released artifacts

Implying allow-pre-launch-script from test-utils meant anything wanting only the
collateral_from_json parser also disabled the arbitrary-root-code check. The
feature is now named on the dependency edges that build tests, so every
enablement is greppable, and a new check fails the build if it reaches the
feature graph of the contract wasm, the node, attestation-cli (which verifies
attestations locally) or the verifier wasm.

Two tests replace one that could not fail: the committed keypair is checked
against near_account_public_key.pub, since create-assets.sh rewrites the public
key but cannot rewrite the secret; and the fixture app-compose is pinned as
satisfying the production policy once its export hook is removed, which is the
only place real attestation data meets the strict check.

Also drops doc claims about sandbox tests and a contract feature that live in
the follow-up, and records why the signer key has to be exported rather than
supplied to the node.
Replaces the helper that took the compile-time policy as a runtime argument
with `PRE_LAUNCH_SCRIPT_ALLOWED`, so every app-compose invariant reads inline
in `validate_app_compose_config`.

Drops the relaxation from this crate's own dev-dependency edge, so its unit
tests compile the production check. The two tests that only existed to pass
both values of the former argument go with it; CI builds `--all-features`, so
the strict branch was never compiled there anyway, and the feature-leak guard
is what keeps the relaxation out of released artifacts.
Multi-path updates replace the eight per-field pipeline stages, the hex
alphabet is bound once, and splitting on the NUL replaces the trim loop.

Records why the two committed fixtures encode the same bytes differently:
`public_data.json` is the endpoint response verbatim, so its byte fields are
JSON arrays, while the fixture parser reads hex.
Restores the Given/When/Then split in the fixture test, and stops the
localnet runbook from restating the collateral's `nextUpdate` date, which
goes stale on regeneration.
Removes `scripts/check-attestation-feature-leak.sh` and its `check-all-fast`
task. The comments that named it as the control now state the invariant it
checked: the relaxation is enabled only on dependency edges that no released
artifact builds.

Also corrects the `local-verify` comment, which claimed `mpc-contract` enables
it; the off-chain callers are the node, the attestation CLI and the TEE
authority.
Adds `export-signer-key-prelaunch.sh`, the `PRELAUNCH_SCRIPT` that exports the
node's in-enclave signer key, so the extraction is reproducible instead of
recoverable only from the fixture's measured app-compose.

`single-node.sh` now resolves a relative `PRELAUNCH_SCRIPT` against the repo
root: `deploy-launcher.sh` reads it from its own directory, so the documented
path was looked up two directories away and the deploy failed.

The runbook drops the steps duplicated from the asset README and gains the
paths the exported key actually lands in.
…hook

The key-export hook drops to 16 lines: the copy into the CVM's shared dir
could never work, since that mount is read-only in the guest, so the console
echo is the only channel. The comment header goes too, as the hook is embedded
verbatim in the measured app-compose.

Collected on a TDX host with the fixture's existing image digests, so the OS
and image measurements are unchanged and only the app-compose-derived values
move: `rtmr3`, `report_data`, the keys and the collateral. `mrtd` and
`rtmr0`-`rtmr2` still match `mpc-attestation`'s compiled-in dev measurements.

`VALID_ATTESTATION_TIMESTAMP` moves into the new collateral's validity window,
which now opens after the previous pinned value.
pbeza added 4 commits August 11, 2026 15:18
Nothing else in these scripts rewrites a user-supplied relative path:
`BASE_PATH` is used verbatim, and `ENV_TPL`/`CONF_TPL` only default to paths
under the repo root. Resolving this one made it behave unlike its neighbours,
so the runbook passes an absolute path instead and `deploy-launcher.sh` fails
fast when it does not exist.

That leaves `single-node.sh` unchanged from main: re-exporting the variable was
a no-op, since a value set on the command line is already in the environment
that `deploy-launcher.sh` inherits.

Also drops the commented-out `PRELAUNCH_SCRIPT` from `default.env`, which the
collection flow never reads — `single-node.sh` overrides `--env-file` — and
trims the warning at the point of use.
Measured rather than guessed: disabling the feature and running without
`--all-features` fails six `validated_dstack_*` tests in `mpc-attestation`, two
in `attestation-cli`, and five in `mpc-contract`, since the regenerated
fixture's app-compose carries the export hook. Noted where they cluster — two
test-file headers, the shared `dstack_verification_setup` helper, and one test —
rather than repeated on all thirteen.

Also renames the fixture test to say the hook is cleared, and rewords the
feature comments that read awkwardly.
The feature sat on `test-utils`' normal dependency edge, so any invocation
whose root set includes that crate — `cargo build --workspace --release`, for
one — unified it into the single `attestation` lib that `mpc-node` and the
contract link. Nothing ships that way today, but a comment was the only thing
saying so.

It now forwards through `mpc-contract` and `attestation-cli` features enabled
only where those crates build their own tests, matching what `mpc-attestation`
already did. `test-utils` no longer needs it: its one verification call is the
DCAP step, which never reaches app-compose validation.

Also fixes the `VALID_ATTESTATION_TIMESTAMP` comment, which still named the
previous value's date, and restores the `local-verify` documentation dropped
earlier in the branch.
/// (spelled in full because the import is feature-gated). The pin exists
/// because sandbox chain time is wall-clock and forward-only, so it can never
/// fall inside the validity window of a checked-in collateral fixture.
fn now_seconds() -> u64 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Production compiles this down to just the block timestamp, so the gating itself is right. But sandbox-test-hooks is a public feature, so a --all-features build turns it on — and here that means expired collateral verifies.

Worth making it unbuildable rather than just unused:

#[cfg(all(target_arch = "wasm32", feature = "sandbox-test-hooks"))]
compile_error!("never enable in a wasm build");

@pbeza
pbeza force-pushed the 4084-verified-attestation-cross-contract branch 2 times, most recently from 4b7ea55 to ab45f90 Compare August 12, 2026 16:23
pbeza added 6 commits August 13, 2026 12:46
Verification hashes the launcher compose and never reads it, so a service that
prints the node's `secrets.json` needs nothing relaxed. That replaces the
app-compose `pre_launch_script`, which production rejects as arbitrary root
code, so `allow-pre-launch-script` and its plumbing are gone from all five
crates and the check is unconditional again — with the rejection test the
feature had made impossible.

Fixtures recaptured on a TDX host with the collection compose. The app-compose
now carries no scripts at all, which a new test asserts.

The contract derives the one legal compose hash from a template compiled into
it, so the fixture's compose is not derivable and five contract tests register
its hash directly. The accommodation is test data rather than a policy switch in
shared code, so nothing can leak into a released artifact.

Also drops three test names from the asset runbook that no longer exist, in
favour of the crates that consume the fixtures.
Comparing the second half of the secret key only read back the public key it
already carries, so a malformed seed passed. Deriving it proves the pair.

Also notes in the asset runbook that the committed key is test-only, since
scanners flagging it will otherwise keep raising the question.
The rust-launcher README pointed readers at step 3 of the asset regeneration
steps, whose command is relative to the directory step 1 changes into, and the
curl snippet used $AGENT_PORT as if single-node.sh exported it. The test command
also omitted tee-authority, which consumes the fixture.
@pbeza
pbeza force-pushed the 4084-verified-attestation-cross-contract branch from ab45f90 to 59555e8 Compare August 13, 2026 22:30
@claude

This comment was marked as low quality.

pbeza added 9 commits August 14, 2026 09:35
The test command runs tests in those crates rather than the crates themselves,
and "the collection variant" named a concept the single-node readme never
introduces.
…ation time

Sandbox chain time is wall-clock and forward-only, so it can never fall inside
the validity window of a checked-in collateral fixture. Under the new test-only
feature, verify_quote prefers a pinned timestamp read from a well-known storage
key that tests write via state patching; the production build reads no storage
at all. The key constant lives in tee-verifier-interface so caller test suites
can share it without depending on the contract crate.
… sandbox

Drives the async submit_participant_info flow against the real deployed
tee-verifier for every verdict: Rejected with a malformed quote, no-verdict
with an undeployed verifier account, and Verified with the fixture quote
against a verifier built with sandbox-test-hooks, whose clock the test pins to
the fixture's validity window.

Covers what the Verified path unlocks: the attestation stores with storage
charged to the contract, the TlsKeyOwnedByOtherAccount guard rejects a second
account claiming the same TLS key, and real dcap-qvl runs within the
production verifier_tera_gas budget with headroom to spare (measured at 175.7
of 200 Tgas). The budget is read from the contract's config view so it cannot
drift from DEFAULT_VERIFIER_TERA_GAS.

Storing a first attestation for a TLS key consumes a prepaid storage grant, so
the tests fund one from a separate payer, leaving the submitter's balance clean
for the only-gas-spent assertions.

Closes #3787
Mirrors the production topology (cf. scripts/launch-localnet.sh). Nodes in e2e
clusters submit mock attestations, which the MPC contract verifies without
calling the verifier, so the verifier stays idle; the cross-contract flow is
covered at the sandbox layer.

The vote-resolution check polls because views can briefly lag final votes, and
MpcClusterConfig::validate now bounds-checks initial_participant_indices, which
also covers the pre-existing indexing in init_contract and add_initial_domains.
The design doc's Testing section described a stub verifier that was built and
then dropped during implementation; it now states the shipped approach, with a
status note recording the supersession. The two verifier runbooks gain a pointer
to the cross-contract sandbox coverage alongside the existing unit test.
DeployedContract::call_from is gone; the typed MpcContractHandle methods that
replaced it do not cover vote_tee_verifier_change yet, so the vote goes through
the surviving explicit-gas variant with a zero deposit.

Also documents in the asset runbook that the sandbox wasm reaches the fixture's
app-compose through mpc-contract/sandbox-test-attestation, and that the tests
signing as the fixture account need the committed secret key.
…y method

The fixture is captured from a CVM whose launcher compose carries the service
that exported its signer key, so its compose hash is not derivable from the
compiled-in template and no vote can allow it. `sandbox-test-attestation` now
gates a method that whitelists that one hash, replacing the app-compose policy
relaxation the feature used to enable.
`verify_quote` takes `&self`, so near-sdk reads the STATE key on every call:
production builds never read the pinned-timestamp key, but they do read
storage. The E2E task runner builds five things, not three, and passes three
WASM paths. The design doc's status line pointed at the shipped design as if it
were the superseded one.
@pbeza
pbeza force-pushed the 4084-verified-attestation-cross-contract branch from 59555e8 to 4e6f821 Compare August 14, 2026 08:24
@claude

This comment was marked as low quality.

Base automatically changed from 3787-regenerate-fixtures to main August 14, 2026 11:10
@claude

This comment was marked as low quality.

@claude

This comment was marked as low quality.

@pbeza

pbeza commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

This comment was marked as outdated.

@claude

This comment was marked as low quality.

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.

Cover the Verified attestation path cross-contract, reusing the real tee-verifier

3 participants