Skip to content

Instantly share code, notes, and snippets.

@silasdavis
Last active July 17, 2026 14:31
Show Gist options
  • Select an option

  • Save silasdavis/7fa0e1fe7010500436ff3ba55ecf3bca to your computer and use it in GitHub Desktop.

Select an option

Save silasdavis/7fa0e1fe7010500436ff3ba55ecf3bca to your computer and use it in GitHub Desktop.
Hefting attested Nitro enclaves: a Solidity lifecycle sketch

Hefting Attested Nitro Enclaves: a Solidity Lifecycle Sketch

SKYNET WARNING: This discussion sketch is substantially AI-generated and requires human review.

This is an explanatory sketch, not production code. It separates two facts about an enclave:

  • Verified means either a ZK proof or an already verified enclave established that an Ethereum EOA belongs to a Nitro enclave running accepted software.
  • Hefted means a trusted operator has associated that enclave with its organisational flock.

Shield counts a state update only when its signer is both verified and hefted to an operator that remains trusted.

There are two verification paths:

  • Proof verification: the enclave submits a ZK proof of its Nitro attestation directly.
  • Vouched verification: an already verified enclave checks the candidate's Nitro quote, matches its commitment against an on-chain quote list, and signs a voucher binding that quote to the candidate EOA.

Lifecycle

  1. A Nitro instance starts and generates an Ethereum EOA inside the enclave.
  2. It produces a Nitro attestation whose statement binds that EOA to the enclave's software identity.
  3. The enclave either submits a ZK proof of that attestation or obtains a voucher from an already verified enclave.
  4. For the vouched path, the vouching enclave checks the candidate's quote against the on-chain quote list and signs the candidate EOA together with the listed quote commitment.
  5. The candidate submits its proof or voucher from its attested EOA.
  6. A trusted operator may heft the EOA before or after verification.
  7. Shield recovers the signer of each state update and checks its current lifecycle eligibility.
  8. The hefting operator may later unheft the enclave, making later updates ineligible.

Verification and hefting are independent:

Attestation verified Operator assigned Counts for Shield
No No No
No Yes No, pending verification
Yes No No, verified but unhefted
Yes Yes Yes, while the operator remains trusted

Verification itself never assigns an operator. A newly verified enclave is therefore unhefted unless an operator already hefted its address. In that pre-hefted case, verification makes the enclave eligible immediately.

Solidity sketch

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

interface IAttestationVerifier {
    // A valid proof binds accepted software to this exact enclave EOA.
    function verify(bytes calldata proof, address enclave) external view returns (bool);
}

interface IOperatorRegistry {
    function isOperator(address account) external view returns (bool);
}

interface IQuoteRegistry {
    function isListed(bytes32 quoteHash) external view returns (bool);
}

interface IEnclaveVoucherVerifier {
    // The voucher binds the candidate EOA and listed quote to the vouching enclave.
    function verifyVoucher(
        address vouchingEnclave,
        address candidateEnclave,
        bytes32 quoteHash,
        bytes calldata voucher
    ) external view returns (bool);
}

interface IEnclaveLifecycle {
    error InvalidAttestationProof(address enclave);
    error QuoteNotListed(bytes32 quoteHash);
    error VouchingEnclaveNotVerified(address enclave);
    error InvalidEnclaveVoucher(address vouchingEnclave, address candidateEnclave);
    error EnclaveAlreadyVerified(address enclave);
    error NotTrustedOperator(address account);
    error ZeroEnclaveAddress();
    error EnclaveAlreadyHefted(address enclave, address operator);
    error NotHeftingOperator(address enclave, address caller);

    event EnclaveVerified(address indexed enclave);
    event EnclaveVouched(
        address indexed enclave, address indexed vouchingEnclave, bytes32 indexed quoteHash
    );
    event EnclaveHefted(address indexed enclave, address indexed operator);
    event EnclaveUnhefted(address indexed enclave, address indexed operator);

    function verifyEnclaveWithProof(bytes calldata proof) external;
    function verifyEnclaveWithVoucher(
        bytes32 quoteHash,
        address vouchingEnclave,
        bytes calldata voucher
    ) external;
    function heft(address enclave) external;
    function unheft(address enclave) external;
    function isEligibleSigner(address enclave) external view returns (bool);
}

contract EnclaveLifecycle is IEnclaveLifecycle {
    IAttestationVerifier public immutable attestationVerifier;
    IOperatorRegistry public immutable operatorRegistry;
    IQuoteRegistry public immutable quoteRegistry;
    IEnclaveVoucherVerifier public immutable voucherVerifier;

    mapping(address enclave => bool isVerified) public verified;
    mapping(address enclave => address operator) public operatorOf;

    constructor(
        IAttestationVerifier attestationVerifier_,
        IOperatorRegistry operatorRegistry_,
        IQuoteRegistry quoteRegistry_,
        IEnclaveVoucherVerifier voucherVerifier_
    ) {
        attestationVerifier = attestationVerifier_;
        operatorRegistry = operatorRegistry_;
        quoteRegistry = quoteRegistry_;
        voucherVerifier = voucherVerifier_;
    }

    function verifyEnclaveWithProof(bytes calldata proof) external {
        address enclave = msg.sender;

        require(!verified[enclave], EnclaveAlreadyVerified(enclave));
        require(attestationVerifier.verify(proof, enclave), InvalidAttestationProof(enclave));

        verified[enclave] = true;
        emit EnclaveVerified(enclave);
    }

    function verifyEnclaveWithVoucher(
        bytes32 quoteHash,
        address vouchingEnclave,
        bytes calldata voucher
    ) external {
        address enclave = msg.sender;

        require(!verified[enclave], EnclaveAlreadyVerified(enclave));
        require(quoteRegistry.isListed(quoteHash), QuoteNotListed(quoteHash));
        require(verified[vouchingEnclave], VouchingEnclaveNotVerified(vouchingEnclave));
        require(
            voucherVerifier.verifyVoucher(vouchingEnclave, enclave, quoteHash, voucher),
            InvalidEnclaveVoucher(vouchingEnclave, enclave)
        );

        verified[enclave] = true;
        emit EnclaveVerified(enclave);
        emit EnclaveVouched(enclave, vouchingEnclave, quoteHash);
    }

    function heft(address enclave) external {
        require(enclave != address(0), ZeroEnclaveAddress());
        require(operatorRegistry.isOperator(msg.sender), NotTrustedOperator(msg.sender));

        address currentOperator = operatorOf[enclave];
        require(
            currentOperator == address(0) || !operatorRegistry.isOperator(currentOperator),
            EnclaveAlreadyHefted(enclave, currentOperator)
        );

        operatorOf[enclave] = msg.sender;
        emit EnclaveHefted(enclave, msg.sender);
    }

    function unheft(address enclave) external {
        address currentOperator = operatorOf[enclave];
        require(currentOperator == msg.sender, NotHeftingOperator(enclave, msg.sender));

        delete operatorOf[enclave];
        emit EnclaveUnhefted(enclave, currentOperator);
    }

    function isEligibleSigner(address enclave) external view returns (bool) {
        address currentOperator = operatorOf[enclave];
        return verified[enclave] && currentOperator != address(0)
            && operatorRegistry.isOperator(currentOperator);
    }
}

The attestation verifier hides Nitro parsing and ZK details. The quote registry represents an on-chain list of accepted quote commitments. The operator registry hides the governance process that maintains trusted organisational units.

Both verification paths use msg.sender as the candidate EOA. A direct proof must bind that address to the attested software. A voucher must be signed by an already verified enclave and bind the same address to a listed quote commitment.

In the vouched path, the vouching enclave performs the detailed quote check off-chain. The lifecycle contract verifies the resulting signature, the vouching enclave's verified status, and membership of the quote commitment in the on-chain list. Because any verified enclave may vouch here, verification is transitive in this sketch.

heft deliberately does not require prior verification. An enclave address has at most one currently trusted operator, while one operator may heft many enclaves. A new trusted operator may replace a claim left by an operator that is no longer trusted.

unheft does not erase verification. It only removes the organisational association, so the same enclave may be hefted again later. It also does not require the caller to remain in the trusted set, allowing a removed operator to clear its own stale claim.

Shield-side use

The lifecycle contract answers only whether a recovered signer is currently eligible. State-update encoding, replay protection, and quorum rules remain Shield concerns.

address enclave = ECDSA.recover(updateDigest, signature);
require(lifecycle.isEligibleSigner(enclave), IneligibleEnclave(enclave));

// Only now count this signature under Shield's state-update rules.

Eligibility is checked when Shield processes the update. This sketch does not model historical eligibility at the time the signature was produced.

Trust assumptions

  • The ZK verifier proves that the attestation binds the exact enclave EOA to accepted software.
  • A vouching enclave checks that the candidate EOA and software match the listed quote before signing its voucher.
  • The voucher signature binds the vouching enclave, candidate EOA, and on-chain quote commitment.
  • The on-chain quote list is an accepted source of quote commitments, but does not itself prove that the off-chain comparison was performed honestly.
  • The enclave keeps control of the EOA private key after registration.
  • IOperatorRegistry accurately reflects the current trusted operator set.
  • Shield treats lifecycle eligibility as one gate in addition to its own update and quorum checks.
  • Verification proves an attested state at registration time, not continuing liveness or continuing key custody.

Deliberate omissions

  • Nitro document parsing, certificate-chain checks, PCR interpretation, and measurement allowlists.
  • ZK circuit design, proof generation, verifier-key management, and proof-system upgrades.
  • Quote-list governance, quote expiry, and limits on transitive vouching.
  • Governance for adding or removing operators and accepted software versions.
  • Attestation expiry, re-attestation, verification revocation, key rotation, and compromise recovery.
  • Relayed registration. The sketch requires the attested EOA to submit its own proof.
  • Operator-to-operator transfer, disputed claims, and cleanup of every enclave when an operator is removed.
  • EIP-712 domains, update schemas, nonces, replay protection, quorum thresholds, and signature aggregation.
  • Upgradeability, deployment, gas optimisation, batching, indexing, and other operational concerns.

The central idea is the intersection: attestation says what an enclave is running, hefting says which trusted organisation stands behind it, and Shield accepts its contribution only while both statements hold.

@njeans

njeans commented Jul 17, 2026

Copy link
Copy Markdown

The vouching path is underspecified in the gist but essentially the vouching enclave would sign off on the measurement and EOA address of the new node. On the contract the new measurement should already be approved. The signature is checked and measurement is checked and the new EOA is recorded.

Also the hefting as it is would have a race condition where an operator can claim another's machine. It would be better to have the operator be the msg.sender of the verifyEnclaveWithVoucher and verifyEnclaveWithProof as the enclave address would not be sending that transaction. The enclave address would come from the proof.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment