diff --git a/contracts/facets/IexecPoco1Facet.sol b/contracts/facets/IexecPoco1Facet.sol index 3116f49b..79fb4c44 100644 --- a/contracts/facets/IexecPoco1Facet.sol +++ b/contracts/facets/IexecPoco1Facet.sol @@ -125,16 +125,17 @@ contract IexecPoco1Facet is if (!_isAccountAuthorizedByRestriction(datasetOrder.requesterrestrict, deal.requester)) { revert IncompatibleDatasetOrder("Requester restriction not satisfied"); } - // The deal's tag should include all tag bits of the dataset order. - // 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) { + // The deal's tag should include all activated tag bits of the dataset order. + // For the dataset tag, bits of TEE frameworks (Scone, Gramine, and TDX) must be ignored + // to allow existing dataset orders with SGX tags to be consumed on TDX workerpools. + // Examples: + // Deal: 0b0101, Dataset: 0b0101 (final: 0b0001) => ok + // Deal: 0b0101, Dataset: 0b0001 (final: 0b0001) => ok + // Deal: 0b0000, Dataset: 0b0001 (final: 0b0001) => !ok + // Cross-framework examples: + // Deal: 0b1001 (TDX), Dataset: 0b0011 (Scone) (final: 0b0001) => ok + bytes32 finalDatasetTag = _ignoreTeeFramework(datasetOrder.tag); + if ((deal.tag & finalDatasetTag) != finalDatasetTag) { revert IncompatibleDatasetOrder("Tag compatibility not satisfied"); } } @@ -229,8 +230,7 @@ contract IexecPoco1Facet is /** * Check orders compatibility */ - - // computation environment & allowed enough funds + // Check computation environment & prices. require(_requestorder.category == _workerpoolorder.category, "iExecV5-matchOrders-0x00"); require(_requestorder.category < $.m_categories.length, "iExecV5-matchOrders-0x01"); require(_requestorder.trust <= _workerpoolorder.trust, "iExecV5-matchOrders-0x02"); @@ -243,14 +243,11 @@ contract IexecPoco1Facet is _requestorder.workerpoolmaxprice >= _workerpoolorder.workerpoolprice, "iExecV5-matchOrders-0x05" ); - // The workerpool tag should include all tag bits of dataset, app, and requester orders. - // 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; + // Check tags compatibility: + // - The workerpool tag should include all activated tag bits of dataset, app, and requester orders. + // - For the dataset tag, bits of TEE frameworks (Scone, Gramine, and TDX) must be ignored + // to allow existing dataset orders with SGX tags to be consumed on TDX workerpools. + bytes32 tag = _apporder.tag | _ignoreTeeFramework(_datasetorder.tag) | _requestorder.tag; require(tag & ~_workerpoolorder.tag == 0x0, "iExecV5-matchOrders-0x06"); require((tag ^ _apporder.tag)[31] & 0x01 == 0x0, "iExecV5-matchOrders-0x07"); @@ -474,4 +471,25 @@ contract IexecPoco1Facet is return dealid; } + + /** + * Ignore TEE framework bits (Scone, Gramine, TDX) in the provided tag to allow + * cross-framework compatibility. + * + * Ignored bit positions in the tag: + * 0b(31 bytes...)1111 + * |||| + * |||└─ bit 0: TEE + * ||└── bit 1: Scone (ignored) + * |└─── bit 2: Gramine (ignored) + * └──── bit 3: TDX (ignored) + * + * @param tag original tag + * @return tag with TEE framework bits ignored + */ + function _ignoreTeeFramework(bytes32 tag) private pure returns (bytes32) { + // (BIT_SCONE | BIT_GRAMINE | BIT_TDX) → 0b 0000 1110 = 0x0E + // Mask = ~0x0E = 0xF1 → 0xFFF...FF1 + return tag & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1; + } } diff --git a/test/byContract/IexecPoco/IexecPoco1.test.ts b/test/byContract/IexecPoco/IexecPoco1.test.ts index 0a4313b3..f12fa18e 100644 --- a/test/byContract/IexecPoco/IexecPoco1.test.ts +++ b/test/byContract/IexecPoco/IexecPoco1.test.ts @@ -17,15 +17,15 @@ import { OwnableMock__factory, } from '../../../typechain'; import { - TAG_ALL_TEE_FRAMEWORKS, + ALL_TEE_TAGS, TAG_BIT_2, TAG_BIT_4, TAG_BIT_4_AND_TEE, + TAG_NAMES, TAG_STANDARD, TAG_TEE, TAG_TEE_GRAMINE, TAG_TEE_SCONE, - TAG_TEE_TDX, } from '../../../utils/constants'; import { IexecOrders, @@ -637,41 +637,26 @@ 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', - ); + /** + * Successful match orders with all combinations of TEE tags in dataset and workerpool orders. + * Validates ignored dataset tag bits. + */ + ALL_TEE_TAGS.forEach((datasetTag) => { + ALL_TEE_TAGS.forEach((workerpoolTag) => { + it(`Should match orders with compatible TEE tags [dataset:${TAG_NAMES[datasetTag]}, workerpool:${TAG_NAMES[workerpoolTag]}]`, 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', + ); + }); }); }); @@ -1163,6 +1148,46 @@ describe('IexecPoco1', () => { ).to.not.be.reverted; }); + /** + * Successful compatibility check with all combinations of TEE tags in deal and dataset order. + * Validates ignored dataset tag bits. + */ + ALL_TEE_TAGS.forEach((datasetTag) => { + ALL_TEE_TAGS.forEach((dealTag) => { + it(`Should not revert with compatible TEE tags [dataset:${TAG_NAMES[datasetTag]}, deal:${TAG_NAMES[dealTag]}]`, async () => { + // Create a new deal with the specified tag, don't reuse the one from beforeEach. + const newDealOrders = buildOrders({ + assets: { ...ordersAssets, dataset: ZeroAddress }, + prices: ordersPrices, + requester: requester.address, + tag: dealTag, + volume: volume, + }); + // Override salts to avoid order consumption conflicts. + newDealOrders.app.salt = ethers.id('app-salt'); + newDealOrders.workerpool.salt = ethers.id('workerpool-salt'); + newDealOrders.requester.salt = ethers.id('requester-salt'); + await depositForRequesterAndSchedulerWithDefaultPrices(volume); + await signOrders(iexecWrapper.getDomain(), newDealOrders, ordersActors); + const dealId = getDealId(iexecWrapper.getDomain(), newDealOrders.requester); + await iexecPocoAsRequester + .matchOrders(...newDealOrders.toArray()) + .then((tx) => tx.wait()); + // Create dataset order with the specified tag + const datasetOrder = { + ...compatibleDatasetOrder, + tag: datasetTag, + salt: ethers.id('dataset-salt'), + }; + await signOrder(iexecWrapper.getDomain(), datasetOrder, datasetProvider); + + // Should not revert because bits in positions 1 to 3 of the dataset order tag are ignored. + await expect(iexecPoco.assertDatasetDealCompatibility(datasetOrder, dealId)).to + .not.be.reverted; + }); + }); + }); + it('Should revert when the dataset order is revoked or fully consumed', async () => { await signOrder(iexecWrapper.getDomain(), compatibleDatasetOrder, datasetProvider); // Revoke order on-chain. @@ -1318,64 +1343,6 @@ describe('IexecPoco1', () => { .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({ diff --git a/utils/constants.ts b/utils/constants.ts index 3c5e0164..510d4bd5 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,14 +1,8 @@ // 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) - */ +// Tag constants. +// For TEE bits positions, see IexecPoco1Facet.sol#_ignoreTeeFramework export const TAG_STANDARD = '0x0000000000000000000000000000000000000000000000000000000000000000'; export const TAG_TEE = '0x0000000000000000000000000000000000000000000000000000000000000001'; export const TAG_TEE_SCONE = '0x0000000000000000000000000000000000000000000000000000000000000003'; // 0b0011 = TEE + Scone @@ -20,6 +14,22 @@ export const TAG_BIT_2 = '0x0000000000000000000000000000000000000000000000000000 export const TAG_BIT_4 = '0x0000000000000000000000000000000000000000000000000000000000000010'; // 0b10000 (bit 4 in 0-indexed) export const TAG_BIT_4_AND_TEE = '0x0000000000000000000000000000000000000000000000000000000000000011'; // 0b10001 +// Tag helpers: +export const ALL_TEE_TAGS = [ + TAG_TEE, + TAG_TEE_SCONE, + TAG_TEE_GRAMINE, + TAG_TEE_TDX, + TAG_ALL_TEE_FRAMEWORKS, +]; +export const TAG_NAMES: { [key: string]: string } = { + [TAG_STANDARD]: 'STANDARD', + [TAG_TEE]: 'TEE', + [TAG_TEE_SCONE]: 'TEE_SCONE', + [TAG_TEE_GRAMINE]: 'TEE_GRAMINE', + [TAG_TEE_TDX]: 'TEE_TDX', + [TAG_ALL_TEE_FRAMEWORKS]: 'TEE_ALL', +}; export const NULL = { BYTES32: '0x0000000000000000000000000000000000000000000000000000000000000000',