test(contract): cover the Verified attestation path cross-contract in sandbox - #4110
Open
pbeza wants to merge 36 commits into
Open
test(contract): cover the Verified attestation path cross-contract in sandbox#4110pbeza wants to merge 36 commits into
pbeza wants to merge 36 commits into
Conversation
|
This PR includes changes to source code files in Suggested title: |
This comment was marked as outdated.
This comment was marked as outdated.
pbeza
force-pushed
the
4084-verified-attestation-cross-contract
branch
from
August 10, 2026 13:56
b5e55c3 to
faea559
Compare
…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.
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.
barakeinav1
reviewed
Aug 12, 2026
| /// (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 { |
Contributor
There was a problem hiding this comment.
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
force-pushed
the
4084-verified-attestation-cross-contract
branch
2 times, most recently
from
August 12, 2026 16:23
4b7ea55 to
ab45f90
Compare
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
force-pushed
the
4084-verified-attestation-cross-contract
branch
from
August 13, 2026 22:30
ab45f90 to
59555e8
Compare
This comment was marked as low quality.
This comment was marked as low quality.
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
force-pushed
the
4084-verified-attestation-cross-contract
branch
from
August 14, 2026 08:24
59555e8 to
4e6f821
Compare
This comment was marked as low quality.
This comment was marked as low quality.
…tion-cross-contract
…tion-cross-contract
This comment was marked as low quality.
This comment was marked as low quality.
This comment was marked as low quality.
This comment was marked as low quality.
Contributor
Author
|
@claude review |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as low quality.
This comment was marked as low quality.
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.
Stacked on #4109. Drives the async
submit_participant_infoflow in sandbox against a real deployedtee-verifier, covering all three outcomes:Rejected, no-verdict (verifier unreachable), andVerified.Closes #4084