Skip to content

Commit 5c28b25

Browse files
authored
refactor(services): unified approveService + TEE commitment-root storage (#119)
Collapse five `approveServiceWith*` entrypoints into one `approveService(ApprovalParams)`. Move TEE commitment storage from a per-operator dynamic array to a single keccak256 root + full data emitted as `TeeCommitmentsRecorded`. BLS stays opt-in via zero pubkey. RFQ paths (`createServiceFromQuotes`, `extendServiceFromQuotes`) are unchanged — they already used the resource-commitment hash pattern. WHY THIS CHANGE The matrix of approval entrypoints was strict superset growth: each new optional capability (commitments → BLS → BLS+commitments → TEE) doubled the surface and left the protocol with a 5-selector explosion that any new feature would extend further. Every variant duplicated the auth gates inline, making it easy to drop a check on one path and ship. The TEE commitment work also stored the full struct array per-operator on activation — a per-(request, operator) cap of 8 commits × 3 storage slots × 20K cold SSTORE ≈ 480K gas per operator just to copy commits forward. With multi-operator services that gas scales linearly in operator count and burns the activator's tx alone, since "last operator pays for everyone" is the default activation pattern. The audit flagged this as a HIGH operator-economics concern. ARCHITECTURE Single entrypoint: approveService(ApprovalParams { requestId, securityCommitments, blsPubkey, blsPopSignature, teeCommitments }) external Empty / zero fields are no-ops. The contract dispatches: - securityCommitments empty + requirements present → auto-fill protocol-default TNT commitment (only when that's the lone requirement) or revert - blsPubkey == [0,0,0,0] → operator skips BLS - teeCommitments empty → operator skips TEE binding Order of operations (fail-fast, validate-before-write): 1. _requireApprovingOperator (auth gate, no SSTORE) 2. _validateSecurityCommitments OR auto-fill default TNT 3. _validateTeeCommitments + compute keccak root 4. BLS proof-of-possession verify (if registering) 5. SSTORE security commits / TEE root / BLS pubkey 6. Mark approved, emit, manager-hook, activate-if-threshold TEE storage shape: mapping(uint64 => mapping(address => bytes32)) _requestTeeCommitmentRoot mapping(uint64 => mapping(address => bytes32)) _serviceTeeCommitmentRoot Activation: O(operators) — one bytes32 SSTORE per operator that supplied a non-empty TEE array. Down from O(operators × commits × 3 slots). Pre-refactor 3-operators × 8-commits activator-gas was 2.89M; post-refactor expected well under 1M (gas test enforces < 1.5M). Slashing pattern: contract stores root only; slasher / provisioning oracle supplies the original commitment array as a witness, verifies `keccak256(abi.encode(witness)) == getTeeCommitmentRoot(serviceId, op)` before treating the witness as authoritative. Same pattern already used by `_serviceResourceCommitmentHash` for RFQ resource commits. RFQ PATHS UNCHANGED `createServiceFromQuotes` and `extendServiceFromQuotes` keep the signed-quote acceptance flow exactly as it was. Quotes carry resource commitments (already hash-stored) and security commitments. TEE is NOT in the EIP-712 quote shape — TEE commitments require a request-derived nonce that doesn't exist until the request is created, so they can't be pre-signed in a quote. Manual approval remains the only path that binds TEE commitments today; if RFQ + TEE is wanted later, extend the QuoteDetails type at that point. BLS IS OPT-IN The protocol must accept any operator. Operators that don't register a BLS pubkey can still approve services and run workloads — they simply cannot participate in aggregated job-result signing on services that choose to use BLS aggregation. The `JobsAggregation` path reverts with `OperatorBlsPubkeyNotRegistered` only when an operator who didn't register tries to participate in a BLS aggregate. No exclusion at approval, no penalty otherwise. API IMPACT - TangleServicesFacet selectors: 10 → 6 Removed: approveService(uint64,uint8), approveServiceWithCommitments, approveServiceWithBls, approveServiceWithCommitmentsAndBls, approveServiceWithTeeCommitments, getTeeCommitment Kept: approveService(ApprovalParams), rejectService, getOperatorBlsPubkey, blsPopMessage, getTeeCommitmentRoot (replaces getTeeCommitment), teeNonceFor - ITangleServices interface trimmed to match. TEST SURFACE Replaced TeeCommitmentApprovalTest.t.sol (16 cases) and TeeCommitmentHardenTest.t.sol (7 cases) with one tight ServicesApprovalTest.t.sol (~14 cases). Every test names a specific failure mode; no compiler-bug theater. Coverage: Happy paths - single-operator + single TEE commit, root persists - minimal approval, no optional fields - mixed TEE / non-TEE operators, roots independent - slasher witness verification (honest matches, tampered rejects) Adversarial — TEE validation - DirectTdx rejected; Unset enum sentinel rejected - zero expectedMeasurement rejected - cross-request replay rejected (nonce request-derived) - past expiry rejected; >MAX_TTL rejected; at-cap accepted - TooManyTeeCommitments (cap = 8) rejected Adversarial — auth ordering - unauthorized caller fails BEFORE storage write - double approval rejected Activation gas measurement - 3 ops × 8 commits gas, asserted < 1.5M (was 2.89M pre-refactor) CALL-SITE MIGRATION ~50 call sites in test/ and script/ migrated to the unified entrypoint via `_approve / _approveWithCommitments / _approveWithBls` helpers hoisted into BlueprintDefinitionHelper (visible in BaseTest, TestHarness, and InvariantFuzz). One inline ApprovalParams construction in three scripts where the helper isn't reachable. DOES NOT TOUCH - fix/slashing-correctness branch / Slashing.sol / SlashingLib.sol - RFQ flows (QuotesCreate, QuotesExtend, TangleQuotesFacet) - BLS aggregation path (JobsAggregation.sol) - Heartbeat, payments, blueprint registry, MBSM, beacon, oracles
1 parent e44681d commit 5c28b25

37 files changed

Lines changed: 829 additions & 1329 deletions

script/CreateServiceForTLV2Test.s.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ contract CreateServiceForTLV2Test is Script {
5050
// 3. Approve service as operator1
5151
console2.log("Approving service as operator1...");
5252
vm.startBroadcast(OPERATOR1_KEY);
53-
tangle.approveService(serviceId, 100); // 100% staking
53+
tangle.approveService(Types.ApprovalParams({requestId: serviceId, securityCommitments: new Types.AssetSecurityCommitment[](0), blsPubkey: [uint256(0),0,0,0], blsPopSignature: [uint256(0),0], teeCommitments: new Types.TeeAttestationCommitment[](0)})); // 100% staking
5454
vm.stopBroadcast();
5555
console2.log("Service approved and active");
5656

script/DemoSimulation.s.sol

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,15 @@ contract DemoSimulation is Script, BlueprintDefinitionHelper {
475475
// Operators approve
476476
for (uint256 j = 0; j < numOps; j++) {
477477
vm.startBroadcast(operatorKeys[j % operators.length]);
478-
tangle.approveService(requestId, uint8(50 + (j * 10))); // 50-70% exposure
478+
tangle.approveService(
479+
Types.ApprovalParams({
480+
requestId: requestId,
481+
securityCommitments: new Types.AssetSecurityCommitment[](0),
482+
blsPubkey: [uint256(0), 0, 0, 0],
483+
blsPopSignature: [uint256(0), 0],
484+
teeCommitments: new Types.TeeAttestationCommitment[](0)
485+
})
486+
);
479487
vm.stopBroadcast();
480488
}
481489

script/LocalTestnet.s.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -913,7 +913,7 @@ contract LocalTestnetSetup is Script, BlueprintDefinitionHelper {
913913
} else {
914914
vm.startPrank(operator1);
915915
}
916-
tangle.approveService(currentRequestId, 50); // 50% staking exposure
916+
tangle.approveService(Types.ApprovalParams({requestId: currentRequestId, securityCommitments: new Types.AssetSecurityCommitment[](0), blsPubkey: [uint256(0),0,0,0], blsPopSignature: [uint256(0),0], teeCommitments: new Types.TeeAttestationCommitment[](0)})); // 50% staking exposure
917917
console2.log("Operator1 approved service", currentRequestId);
918918
if (useBroadcastKeys) {
919919
vm.stopBroadcast();
@@ -926,7 +926,7 @@ contract LocalTestnetSetup is Script, BlueprintDefinitionHelper {
926926
} else {
927927
vm.startPrank(operator2);
928928
}
929-
tangle.approveService(currentRequestId, 50); // 50% staking exposure
929+
tangle.approveService(Types.ApprovalParams({requestId: currentRequestId, securityCommitments: new Types.AssetSecurityCommitment[](0), blsPubkey: [uint256(0),0,0,0], blsPopSignature: [uint256(0),0], teeCommitments: new Types.TeeAttestationCommitment[](0)})); // 50% staking exposure
930930
console2.log("Operator2 approved service", currentRequestId);
931931
if (useBroadcastKeys) {
932932
vm.stopBroadcast();
@@ -1251,7 +1251,7 @@ contract LocalTestnetSetup is Script, BlueprintDefinitionHelper {
12511251
if (useBroadcastKeys) vm.startBroadcast(operatorKey);
12521252
else vm.startPrank(operator);
12531253

1254-
tangle.approveService(reqId, 50);
1254+
tangle.approveService(Types.ApprovalParams({requestId: reqId, securityCommitments: new Types.AssetSecurityCommitment[](0), blsPubkey: [uint256(0),0,0,0], blsPopSignature: [uint256(0),0], teeCommitments: new Types.TeeAttestationCommitment[](0)}));
12551255

12561256
if (useBroadcastKeys) vm.stopBroadcast();
12571257
else vm.stopPrank();

src/TangleStorage.sol

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -373,14 +373,19 @@ abstract contract TangleStorage {
373373
// TEE ATTESTATION COMMITMENTS
374374
// ═══════════════════════════════════════════════════════════════════════════
375375

376-
/// @notice Request ID => Operator => TEE attestation commitments captured at approval.
377-
/// @dev Transferred to `_serviceTeeCommitments` on activation; not used afterwards.
378-
mapping(uint64 => mapping(address => Types.TeeAttestationCommitment[])) internal _requestTeeCommitments;
379-
380-
/// @notice Service ID => Operator => TEE attestation commitments persisted at activation.
381-
/// @dev Read by blueprint contracts during their provisioning hooks to cross-check the
382-
/// live TEE attestation against the operator's on-chain commitment.
383-
mapping(uint64 => mapping(address => Types.TeeAttestationCommitment[])) internal _serviceTeeCommitments;
376+
/// @notice Request ID => Operator => keccak256 root over the operator's
377+
/// TeeAttestationCommitment[] supplied at approval. Cleared after
378+
/// activation copies the value forward.
379+
/// @dev Full array is emitted in `TeeCommitmentsRecorded` so any indexer or
380+
/// slashing witness can reconstruct it. Storing the root keeps activation
381+
/// gas O(operators) instead of O(operators × commitments × slots).
382+
mapping(uint64 => mapping(address => bytes32)) internal _requestTeeCommitmentRoot;
383+
384+
/// @notice Service ID => Operator => keccak256 root over the operator's
385+
/// TeeAttestationCommitment[]. Read by slashing / blueprint provisioning
386+
/// hooks; the original array is supplied as a witness and verified
387+
/// against this root.
388+
mapping(uint64 => mapping(address => bytes32)) internal _serviceTeeCommitmentRoot;
384389

385390
// ═══════════════════════════════════════════════════════════════════════════
386391
// RESERVED STORAGE GAP

0 commit comments

Comments
 (0)