diff --git a/contracts/facets/IexecPoco1Facet.sol b/contracts/facets/IexecPoco1Facet.sol index d3091ed4..ecea5b18 100644 --- a/contracts/facets/IexecPoco1Facet.sol +++ b/contracts/facets/IexecPoco1Facet.sol @@ -126,10 +126,15 @@ contract IexecPoco1Facet is revert IncompatibleDatasetOrder("Requester restriction not satisfied"); } // The deal's tag should include all tag bits of the dataset order. - // Deal: 0b0101, Dataset: 0b0101 => ok - // Deal: 0b0101, Dataset: 0b0001 => ok - // Deal: 0b0101, Dataset: 0b0010 => !ok - if ((deal.tag & datasetOrder.tag) != datasetOrder.tag) { + // For dataset orders: ignore Scone, Gramine, and TDX framework bits to allow + // dataset orders from SGX workerpools to be consumed on TDX workerpools and vice versa. + // Examples after masking: + // Deal: 0b0101, Dataset: 0b0101 => Masked Dataset: 0b0001 => ok + // Deal: 0b0101, Dataset: 0b0001 => Masked Dataset: 0b0001 => ok + // Deal: 0b1001 (TDX), Dataset: 0b0011 (Scone) => Masked Dataset: 0b0001 => ok (cross-framework compatibility) + bytes32 maskedDatasetTag = datasetOrder.tag & + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1; + if ((deal.tag & maskedDatasetTag) != maskedDatasetTag) { revert IncompatibleDatasetOrder("Tag compatibility not satisfied"); } } @@ -239,7 +244,13 @@ contract IexecPoco1Facet is "iExecV5-matchOrders-0x05" ); // The workerpool tag should include all tag bits of dataset, app, and requester orders. - bytes32 tag = _apporder.tag | _datasetorder.tag | _requestorder.tag; + // For dataset orders: ignore Scone, Gramine, and TDX framework bits to allow + // dataset orders from SGX workerpools to be consumed on TDX workerpools and vice versa. + // Bit positions: bit 0 = TEE, bit 1 = Scone, bit 2 = Gramine, bit 3 = TDX + // Mask: ~(BIT_SCONE | BIT_GRAMINE | BIT_TDX) = ~0xE = 0xFFF...FF1 + bytes32 maskedDatasetTag = _datasetorder.tag & + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1; + bytes32 tag = _apporder.tag | maskedDatasetTag | _requestorder.tag; require(tag & ~_workerpoolorder.tag == 0x0, "iExecV5-matchOrders-0x06"); require((tag ^ _apporder.tag)[31] & 0x01 == 0x0, "iExecV5-matchOrders-0x07"); diff --git a/test/byContract/IexecPoco/IexecPoco1.test.ts b/test/byContract/IexecPoco/IexecPoco1.test.ts index 3e67d8d4..0a4313b3 100644 --- a/test/byContract/IexecPoco/IexecPoco1.test.ts +++ b/test/byContract/IexecPoco/IexecPoco1.test.ts @@ -16,7 +16,17 @@ import { IexecPocoAccessors__factory, OwnableMock__factory, } from '../../../typechain'; -import { TAG_STANDARD, TAG_TEE } from '../../../utils/constants'; +import { + TAG_ALL_TEE_FRAMEWORKS, + TAG_BIT_2, + TAG_BIT_4, + TAG_BIT_4_AND_TEE, + TAG_STANDARD, + TAG_TEE, + TAG_TEE_GRAMINE, + TAG_TEE_SCONE, + TAG_TEE_TDX, +} from '../../../utils/constants'; import { IexecOrders, OrdersActors, @@ -627,6 +637,44 @@ describe('IexecPoco1', () => { ); }); + [ + { + datasetTag: TAG_TEE_SCONE, + workerpoolTag: TAG_TEE_TDX, + description: 'Scone tag (0x3) and workerpool has TDX tag (0x9)', + }, + { + datasetTag: TAG_TEE_GRAMINE, + workerpoolTag: TAG_TEE_TDX, + description: 'Gramine tag (0x5) and workerpool has TDX tag (0x9)', + }, + { + datasetTag: TAG_TEE_TDX, + workerpoolTag: TAG_TEE_SCONE, + description: 'TDX tag (0x9) and workerpool has Scone tag (0x3)', + }, + { + datasetTag: TAG_ALL_TEE_FRAMEWORKS, + workerpoolTag: TAG_TEE, + description: 'all TEE framework bits (0xF) and workerpool has TEE only (0x1)', + }, + ].forEach(({ datasetTag, workerpoolTag, description }) => { + it(`Should match orders when dataset has ${description}`, async () => { + orders.dataset.tag = datasetTag; + orders.workerpool.tag = workerpoolTag; + orders.app.tag = TAG_TEE; + orders.requester.tag = TAG_TEE; + + await depositForRequesterAndSchedulerWithDefaultPrices(volume); + await signOrders(iexecWrapper.getDomain(), orders, ordersActors); + + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.emit( + iexecPoco, + 'OrdersMatched', + ); + }); + }); + // TODO add success tests for: // - identity groups // - pre-signatures @@ -680,32 +728,32 @@ describe('IexecPoco1', () => { }); it('Should fail when workerpool tag does not satisfy app, dataset and request requirements', async () => { - orders.app.tag = '0x0000000000000000000000000000000000000000000000000000000000000001'; // 0b0001 - orders.dataset.tag = - '0x0000000000000000000000000000000000000000000000000000000000000002'; // 0b0010 - orders.requester.tag = - '0x0000000000000000000000000000000000000000000000000000000000000003'; // 0b0011 - // Workerpool order is supposed to satisfy conditions of all actors. - // Bad tag, correct tag should be 0b0011. - orders.workerpool.tag = - '0x0000000000000000000000000000000000000000000000000000000000000004'; // 0b0100 - // Match orders. + orders.app.tag = TAG_TEE; //0b0001 + orders.dataset.tag = TAG_TEE_SCONE; // 0b0011 + orders.requester.tag = TAG_TEE_SCONE; // 0b0011 + orders.workerpool.tag = TAG_BIT_2; // 0b0100 - does not satisfy last bits of app, dataset, request + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( + 'iExecV5-matchOrders-0x06', + ); + }); + + it('Should fail when dataset has other bits set that are not ignored', async () => { + orders.dataset.tag = TAG_BIT_4_AND_TEE; // 0b10001 bit 4 set (not ignored) + orders.workerpool.tag = TAG_TEE; + orders.app.tag = TAG_TEE; + orders.requester.tag = TAG_TEE; + await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( 'iExecV5-matchOrders-0x06', ); }); it('Should fail when the last bit of app tag does not satisfy dataset or request requirements', async () => { - // The last bit of dataset and request tag is 1, but app tag does not set it - orders.app.tag = '0x0000000000000000000000000000000000000000000000000000000000000002'; // 0b0010 - orders.dataset.tag = - '0x0000000000000000000000000000000000000000000000000000000000000003'; // 0b0011 - orders.requester.tag = - '0x0000000000000000000000000000000000000000000000000000000000000003'; // 0b0011 + orders.app.tag = TAG_BIT_2; // 0b0100 + orders.dataset.tag = TAG_TEE_GRAMINE; // 0b0101 + orders.requester.tag = TAG_TEE_GRAMINE; // Set the workerpool tag in a way to pass first tag check. - orders.workerpool.tag = - '0x0000000000000000000000000000000000000000000000000000000000000003'; // 0b0011 - // Match orders. + orders.workerpool.tag = TAG_TEE_GRAMINE; await expect(iexecPocoAsRequester.matchOrders(...orders.toArray())).to.be.revertedWith( 'iExecV5-matchOrders-0x07', ); @@ -1253,11 +1301,11 @@ describe('IexecPoco1', () => { .withArgs('Requester restriction not satisfied'); }); - it('Should revert when tag compatibility is not satisfied', async () => { + it('Should revert when tag compatibility with deal is not satisfied', async () => { // Create dataset order with incompatible tag const incompatibleTagDatasetOrder = { ...compatibleDatasetOrder, - tag: '0x0000000000000000000000000000000000000000000000000000000000000010', // Different tag + tag: TAG_BIT_4, // Different tag }; await signOrder(iexecWrapper.getDomain(), incompatibleTagDatasetOrder, datasetProvider); await expect( @@ -1269,6 +1317,95 @@ describe('IexecPoco1', () => { .to.be.revertedWithCustomError(iexecPoco, 'IncompatibleDatasetOrder') .withArgs('Tag compatibility not satisfied'); }); + + // TODO: Add more test cases for tag compatibility + [ + { + datasetTag: TAG_TEE_SCONE, + dealTag: TAG_TEE_TDX, + description: 'Scone tag (0x3) and deal has TDX tag (0x9)', + saltPrefix: 'scone-tdx', + }, + { + datasetTag: TAG_TEE_GRAMINE, + dealTag: TAG_TEE_TDX, + description: 'Gramine tag (0x5) and deal has TDX tag (0x9)', + saltPrefix: 'gramine-tdx', + }, + { + datasetTag: TAG_TEE_TDX, + dealTag: TAG_TEE_SCONE, + description: 'TDX tag (0x9) and deal has Scone tag (0x3)', + saltPrefix: 'tdx-scone', + }, + { + datasetTag: TAG_ALL_TEE_FRAMEWORKS, + dealTag: TAG_TEE, + description: 'all TEE framework bits (0xF) and deal has TEE only (0x1)', + saltPrefix: 'all-frameworks-tee', + }, + ].forEach(({ datasetTag, dealTag, description, saltPrefix }) => { + it(`Should not revert when dataset has ${description}`, async () => { + // Create a deal with the specified tag + const dealOrders = buildOrders({ + assets: { ...ordersAssets, dataset: ZeroAddress }, + prices: ordersPrices, + requester: requester.address, + tag: dealTag, + volume: volume, + }); + dealOrders.app.salt = ethers.id(`${saltPrefix}-app-salt`); + dealOrders.workerpool.salt = ethers.id(`${saltPrefix}-workerpool-salt`); + dealOrders.requester.salt = ethers.id(`${saltPrefix}-requester-salt`); + await depositForRequesterAndSchedulerWithDefaultPrices(volume); + await signOrders(iexecWrapper.getDomain(), dealOrders, ordersActors); + const dealId = getDealId(iexecWrapper.getDomain(), dealOrders.requester); + await iexecPocoAsRequester.matchOrders(...dealOrders.toArray()); + + // Create dataset order with the specified tag + const datasetOrder = { + ...compatibleDatasetOrder, + tag: datasetTag, + salt: ethers.id(`${saltPrefix}-dataset-salt`), + }; + await signOrder(iexecWrapper.getDomain(), datasetOrder, datasetProvider); + + // Should not revert because bits 1-3 of dataset tag are ignored + await expect(iexecPoco.assertDatasetDealCompatibility(datasetOrder, dealId)).to.not + .be.reverted; + }); + }); + + it('Should revert when dataset has bit 4 set (not masked) and deal does not', async () => { + // Create a deal with TEE only (0b0001 = 0x1) + const teeOnlyOrders = buildOrders({ + assets: { ...ordersAssets, dataset: ZeroAddress }, + prices: ordersPrices, + requester: requester.address, + tag: TAG_TEE, // TEE only + volume: volume, + }); + teeOnlyOrders.app.salt = ethers.id('tee-bit4-app-salt'); + teeOnlyOrders.workerpool.salt = ethers.id('tee-bit4-workerpool-salt'); + teeOnlyOrders.requester.salt = ethers.id('tee-bit4-requester-salt'); + await depositForRequesterAndSchedulerWithDefaultPrices(volume); + await signOrders(iexecWrapper.getDomain(), teeOnlyOrders, ordersActors); + const teeOnlyDealId = getDealId(iexecWrapper.getDomain(), teeOnlyOrders.requester); + await iexecPocoAsRequester.matchOrders(...teeOnlyOrders.toArray()); + + // Create dataset order with bit 4 set (0b10001 = 0x11) + const bit4DatasetOrder = { + ...compatibleDatasetOrder, + tag: TAG_BIT_4_AND_TEE, + salt: ethers.id('bit4-dataset-salt'), + }; + await signOrder(iexecWrapper.getDomain(), bit4DatasetOrder, datasetProvider); + + // Should revert because bit 4 is NOT masked and the deal doesn't have it + await expect(iexecPoco.assertDatasetDealCompatibility(bit4DatasetOrder, teeOnlyDealId)) + .to.be.revertedWithCustomError(iexecPoco, 'IncompatibleDatasetOrder') + .withArgs('Tag compatibility not satisfied'); + }); }); /** diff --git a/utils/constants.ts b/utils/constants.ts index ff4d4b8f..3c5e0164 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,8 +1,25 @@ // SPDX-FileCopyrightText: 2020-2025 IEXEC BLOCKCHAIN TECH // SPDX-License-Identifier: Apache-2.0 +/** + * Tag constants: + * - Bit 0: TEE + * - Bit 1: Scone + * - Bit 2: Gramine + * - Bit 3: TDX + * - Bit 4: GPU (0x10) + */ export const TAG_STANDARD = '0x0000000000000000000000000000000000000000000000000000000000000000'; export const TAG_TEE = '0x0000000000000000000000000000000000000000000000000000000000000001'; +export const TAG_TEE_SCONE = '0x0000000000000000000000000000000000000000000000000000000000000003'; // 0b0011 = TEE + Scone +export const TAG_TEE_GRAMINE = '0x0000000000000000000000000000000000000000000000000000000000000005'; // 0b0101 = TEE + Gramine +export const TAG_TEE_TDX = '0x0000000000000000000000000000000000000000000000000000000000000009'; // 0b1001 = TEE + TDX +export const TAG_ALL_TEE_FRAMEWORKS = + '0x000000000000000000000000000000000000000000000000000000000000000F'; // 0b1111 = TEE + Scone + Gramine + TDX +export const TAG_BIT_2 = '0x0000000000000000000000000000000000000000000000000000000000000004'; // 0b0100 +export const TAG_BIT_4 = '0x0000000000000000000000000000000000000000000000000000000000000010'; // 0b10000 (bit 4 in 0-indexed) +export const TAG_BIT_4_AND_TEE = + '0x0000000000000000000000000000000000000000000000000000000000000011'; // 0b10001 export const NULL = { BYTES32: '0x0000000000000000000000000000000000000000000000000000000000000000',