diff --git a/CHANGELOG.md b/CHANGELOG.md index 440e61ca7..b8c239ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - 000_fullchain-5workers-1error.js (#160, #162) - Clean ToDo (#163) - 200_fullchain-bot.js (#164, #166) + - 201_fullchain-bot-dualPool.js (#171) - Fix balance checks in integration tests (#165) - Remove `smock` from unit tests: - IexecEscrow.v8 (#154, #155) diff --git a/test/000_fullchain.test.ts b/test/000_fullchain.test.ts index c06c4046f..1b352c132 100644 --- a/test/000_fullchain.test.ts +++ b/test/000_fullchain.test.ts @@ -118,8 +118,9 @@ describe('Integration tests', function () { volume, trust: workers.length ** 2 - 1, }); - const { dealId, dealPrice, schedulerStakePerDeal } = - await iexecWrapper.signAndSponsorMatchOrders(...orders.toArray()); + const { dealId, schedulerStakePerDeal } = await iexecWrapper.signAndSponsorMatchOrders( + ...orders.toArray(), + ); const taskPrice = appPrice + datasetPrice + workerpoolPrice; const schedulerStakePerTask = schedulerStakePerDeal / volume; const workersRewardPerTask = await iexecWrapper.computeWorkersRewardPerTask( @@ -209,7 +210,7 @@ describe('Integration tests', function () { volume, trust: workers.length ** 2 - 1, }); - const { dealId, dealPrice, schedulerStakePerDeal } = await iexecWrapper.signAndMatchOrders( + const { dealId, schedulerStakePerDeal } = await iexecWrapper.signAndMatchOrders( ...orders.toArray(), ); const taskPrice = appPrice + datasetPrice + workerpoolPrice; @@ -298,8 +299,9 @@ describe('Integration tests', function () { volume, trust: 1, }); - const { dealId, dealPrice, schedulerStakePerDeal } = - await iexecWrapper.signAndSponsorMatchOrders(...orders.toArray()); + const { dealId, schedulerStakePerDeal } = await iexecWrapper.signAndSponsorMatchOrders( + ...orders.toArray(), + ); const taskPrice = appPrice + datasetPrice + workerpoolPrice; const schedulerStakePerTask = schedulerStakePerDeal / volume; const workersRewardPerTask = await iexecWrapper.computeWorkersRewardPerTask( @@ -376,7 +378,7 @@ describe('Integration tests', function () { volume, trust: 1, }); - const { dealId, dealPrice, schedulerStakePerDeal } = await iexecWrapper.signAndMatchOrders( + const { dealId, schedulerStakePerDeal } = await iexecWrapper.signAndMatchOrders( ...orders.toArray(), ); const taskPrice = appPrice + datasetPrice + workerpoolPrice; @@ -601,7 +603,8 @@ describe('Integration tests', function () { }); } }); - it(`[7] No sponsorship, no beneficiary, no callback, no BoT, up to 5 workers with 1 bad worker`, async function () { + + it('[7] No sponsorship, no beneficiary, no callback, no BoT, up to 5 workers with 1 bad worker', async function () { const volume = 1; const allWorkers = [worker1, worker2, worker3, worker4, worker5]; const { resultDigest: badResultDigest } = buildUtf8ResultAndDigest('bad-result'); diff --git a/test/201_fullchain-multi-orders.test.ts b/test/201_fullchain-multi-orders.test.ts new file mode 100644 index 000000000..3213da730 --- /dev/null +++ b/test/201_fullchain-multi-orders.test.ts @@ -0,0 +1,255 @@ +// SPDX-FileCopyrightText: 2024 IEXEC BLOCKCHAIN TECH +// SPDX-License-Identifier: Apache-2.0 + +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { ethers, expect } from 'hardhat'; +import { loadHardhatFixtureDeployment } from '../scripts/hardhat-fixture-deployer'; +import { + IexecInterfaceNative, + IexecInterfaceNative__factory, + IexecPocoAccessors, + IexecPocoAccessors__factory, +} from '../typechain'; +import { + IexecOrders, + OrdersActors, + OrdersAssets, + OrdersPrices, + buildOrders, +} from '../utils/createOrders'; +import { + PocoMode, + TaskStatusEnum, + buildUtf8ResultAndDigest, + getIexecAccounts, +} from '../utils/poco-tools'; +import { IexecWrapper } from './utils/IexecWrapper'; + +const standardDealTag = '0x0000000000000000000000000000000000000000000000000000000000000000'; +const appPrice = 1000; +const datasetPrice = 1_000_000; +const workerpoolPrice1 = 1_000_000_015; +const workerpoolPrice2 = 1_000_000_025; +const { results, resultDigest } = buildUtf8ResultAndDigest('result'); + +let proxyAddress: string; +let iexecPoco: IexecInterfaceNative; +let iexecPocoAccessors: IexecPocoAccessors; // To use `computeDealVolume()` +let iexecWrapper: IexecWrapper; +let [appAddress, workerpoolAddress, datasetAddress]: string[] = []; +let [requester, appProvider, datasetProvider, scheduler, anyone, worker1]: SignerWithAddress[] = []; +let ordersActors: OrdersActors; +let ordersAssets: OrdersAssets; +let ordersPrices: OrdersPrices; + +describe('Integration tests', function () { + beforeEach('Deploy', async () => { + // Deploy all contracts + proxyAddress = await loadHardhatFixtureDeployment(); + // Initialize test environment + await loadFixture(initFixture); + }); + + async function initFixture() { + const accounts = await getIexecAccounts(); + ({ requester, appProvider, datasetProvider, scheduler, anyone, worker1 } = accounts); + iexecWrapper = new IexecWrapper(proxyAddress, accounts); + ({ appAddress, datasetAddress, workerpoolAddress } = await iexecWrapper.createAssets()); + iexecPoco = IexecInterfaceNative__factory.connect(proxyAddress, anyone); + iexecPocoAccessors = IexecPocoAccessors__factory.connect(proxyAddress, ethers.provider); + ordersActors = { + appOwner: appProvider, + datasetOwner: datasetProvider, + workerpoolOwner: scheduler, + requester: requester, + }; + ordersAssets = { + app: appAddress, + dataset: datasetAddress, + workerpool: workerpoolAddress, + }; + ordersPrices = { + app: appPrice, + dataset: datasetPrice, + workerpool: 0, // Overridden below. + }; + } + + /** + * A test to run full workflow (matchOrders..finalize) with 2 orders having 2 different volumes + * for the same workerpool and only 1 request order. + */ + it('[1] No sponsorship, no beneficiary, no callback, BoT, no replication, 2 workerpool orders', async function () { + const volume = 3; + const workerpoolOrderVolume1 = 2; + const workerpoolOrderVolume2 = 10; + const dealVolume1 = Math.min(workerpoolOrderVolume1, volume); // min(2, 3); + const dealVolume2 = Math.min(workerpoolOrderVolume2, volume - dealVolume1); // min(10, 1) + const taskPrice1 = appPrice + datasetPrice + workerpoolPrice1; + const taskPrice2 = appPrice + datasetPrice + workerpoolPrice2; + // Create default orders. + const { + appOrder, + datasetOrder, + workerpoolOrder, + requesterOrder: requestOrder, + } = buildOrders({ + assets: ordersAssets, + prices: ordersPrices, + requester: requester.address, + tag: standardDealTag, + volume, + }).toObject(); + // Create 2 different orders for the same workerpool. + const workerpoolOrder1 = { ...workerpoolOrder }; // Shallow cloning is fine here. + const workerpoolOrder2 = { ...workerpoolOrder }; + workerpoolOrder1.volume = workerpoolOrderVolume1; + workerpoolOrder1.workerpoolprice = workerpoolPrice1; + workerpoolOrder2.volume = workerpoolOrderVolume2; + workerpoolOrder2.workerpoolprice = workerpoolPrice2; + requestOrder.workerpoolmaxprice = Math.max(workerpoolPrice1, workerpoolPrice2); + // Match both workerpool orders with the same request order. + const dealOrders1 = new IexecOrders( + appOrder, + datasetOrder, + workerpoolOrder1, + requestOrder, + ).toArray(); + const dealOrders2 = new IexecOrders( + appOrder, + datasetOrder, + workerpoolOrder2, + requestOrder, + ).toArray(); + expect(await iexecPocoAccessors.computeDealVolume(...dealOrders1)).to.equal(dealVolume1); + const { + dealId: dealId1, + taskIndex: taskIndex1, + schedulerStakePerDeal: schedulerStakeForDeal1, + } = await iexecWrapper.signAndMatchOrders( + appOrder, + datasetOrder, + workerpoolOrder1, + requestOrder, + ); // First task index is 0. + expect(await iexecPocoAccessors.computeDealVolume(...dealOrders2)).to.equal(dealVolume2); + const { + dealId: dealId2, + taskIndex: taskIndex2, + schedulerStakePerDeal: schedulerStakeForDeal2, + } = await iexecWrapper.signAndMatchOrders( + appOrder, + datasetOrder, + workerpoolOrder2, + requestOrder, + ); // First task index is 2. + const deal1 = await iexecPoco.viewDeal(dealId1); + expect(deal1.botFirst).to.equal(0); + expect(deal1.botSize).to.equal(dealVolume1); + const deal2 = await iexecPoco.viewDeal(dealId2); + expect(deal2.botFirst).to.equal(dealVolume1); + expect(deal2.botSize).to.equal(dealVolume2); + // Compute stakes and rewards for each deal. + const schedulerStakePerTaskOfDeal1 = schedulerStakeForDeal1 / dealVolume1; + const schedulerStakePerTaskOfDeal2 = schedulerStakeForDeal2 / dealVolume2; + const workersRewardPerTaskOfDeal1 = await iexecWrapper.computeWorkersRewardPerTask( + dealId1, + PocoMode.CLASSIC, + ); + const workersRewardPerTaskOfDeal2 = await iexecWrapper.computeWorkersRewardPerTask( + dealId2, + PocoMode.CLASSIC, + ); + const schedulerRewardPerTaskOfDeal1 = workerpoolPrice1 - workersRewardPerTaskOfDeal1; + const schedulerRewardPerTaskOfDeal2 = workerpoolPrice2 - workersRewardPerTaskOfDeal2; + // Finalize each task and run checks. + await runTaskThenCheckBalancesAndVolumes( + dealId1, + taskIndex1, + taskPrice1, + schedulerStakePerTaskOfDeal1, + schedulerRewardPerTaskOfDeal1, + workersRewardPerTaskOfDeal1, + ); + await runTaskThenCheckBalancesAndVolumes( + dealId1, + taskIndex1 + 1, + taskPrice1, + schedulerStakePerTaskOfDeal1, + schedulerRewardPerTaskOfDeal1, + workersRewardPerTaskOfDeal1, + ); + await runTaskThenCheckBalancesAndVolumes( + dealId2, + taskIndex2, + taskPrice2, + schedulerStakePerTaskOfDeal2, + schedulerRewardPerTaskOfDeal2, + workersRewardPerTaskOfDeal2, + ); + // Check remaining volumes. + expect(await iexecPoco.viewConsumed(iexecWrapper.hashOrder(requestOrder))).to.equal(volume); + expect(await iexecPoco.viewConsumed(iexecWrapper.hashOrder(workerpoolOrder1))).to.equal( + dealVolume1, + ); + expect(await iexecPoco.viewConsumed(iexecWrapper.hashOrder(workerpoolOrder2))).to.equal( + dealVolume2, + ); + }); + + async function runTaskThenCheckBalancesAndVolumes( + dealId: string, + taskIndex: number, + taskPrice: number, + schedulerStake: number, + schedulerReward: number, + workerReward: number, + ) { + // Save frozens before task execution. + const accounts = [requester, scheduler, appProvider, datasetProvider, worker1]; + const accountsInitialFrozens = await iexecWrapper.getInitialFrozens(accounts); + // Run task. + const taskId = await iexecWrapper.initializeTask(dealId, taskIndex); + const { workerStakePerTask: workerStake } = await iexecWrapper.contributeToTask( + dealId, + taskIndex, + resultDigest, + worker1, + ); + await iexecPoco + .connect(worker1) + .reveal(taskId, resultDigest) + .then((tx) => tx.wait()); + const finalizeTx = await iexecPoco.connect(scheduler).finalize(taskId, results, '0x'); + await finalizeTx.wait(); + // Check task. + const task = await iexecPoco.viewTask(taskId); + expect(task.status).to.equal(TaskStatusEnum.COMPLETED); + expect(task.idx).to.equal(taskIndex); + // Verify token balance changes. + const expectedProxyBalanceChange = -(taskPrice + schedulerStake + workerStake); + await expect(finalizeTx).to.changeTokenBalances( + iexecPoco, + [proxyAddress, requester, scheduler, appProvider, datasetProvider, worker1], + [ + expectedProxyBalanceChange, // Proxy + 0, // Requester + schedulerStake + schedulerReward, // Scheduler + appPrice, // AppProvider + datasetPrice, // DatasetProvider + workerStake + workerReward, // Worker + ], + ); + // Calculate expected frozen changes + const expectedFrozenChanges = [ + 0, // Proxy + -taskPrice, // Requester + -schedulerStake, // Scheduler + 0, // AppProvider + 0, // DatasetProvider + 0, // Worker + ]; + await iexecWrapper.checkFrozenChanges(accountsInitialFrozens, expectedFrozenChanges); + } +}); diff --git a/test/byContract/IexecPoco/IexecPoco1.test.ts b/test/byContract/IexecPoco/IexecPoco1.test.ts index 394293edf..61d4abfd2 100644 --- a/test/byContract/IexecPoco/IexecPoco1.test.ts +++ b/test/byContract/IexecPoco/IexecPoco1.test.ts @@ -638,6 +638,8 @@ describe('IexecPoco1', () => { // - identity groups // - pre-signatures // - low orders volumes + // - test when the lowest volume is in one of the orders + // - test when the lowest volume in order < unconsumed volume // - multiple matches of the same order it('Should fail when categories are different', async () => { diff --git a/test/utils/IexecWrapper.ts b/test/utils/IexecWrapper.ts index f5ca9e840..124636828 100644 --- a/test/utils/IexecWrapper.ts +++ b/test/utils/IexecWrapper.ts @@ -17,6 +17,7 @@ import { IexecLibOrders_v5, IexecMaintenanceDelegate__factory, IexecPoco2__factory, + IexecPocoAccessors__factory, IexecPocoBoostAccessors__factory, RLC__factory, WorkerpoolRegistry, @@ -135,7 +136,7 @@ export class IexecWrapper { this.accounts.anyone, ).m_workerStakeRatioPolicy() ).toNumber(); - return (workerpoolPrice * workerStakeRatio) / 100; + return Math.floor((workerpoolPrice * workerStakeRatio) / 100); } /** @@ -153,8 +154,8 @@ export class IexecWrapper { } /** - * Compute the amount of RLC tokens that the worker receives - * as a reward per task. + * Compute the amount of RLC tokens that are rewarded to workers when + * a task is finalized. * @param dealId * @param mode * @returns @@ -173,10 +174,9 @@ export class IexecWrapper { this.proxyAddress, ethers.provider, ).viewDeal(dealId); - // (workerpoolPrice * workerRatio) / 100 - return ( - (deal.workerpool.price.toNumber() * (100 - deal.schedulerRewardRatio.toNumber())) / 100 - ); + // reward = (workerpoolPrice * workersRatio) / 100 + const workersRewardRatio = 100 - deal.schedulerRewardRatio.toNumber(); + return Math.floor((deal.workerpool.price.toNumber() * workersRewardRatio) / 100); } async setTeeBroker(brokerAddress: string) { @@ -256,14 +256,18 @@ export class IexecWrapper { const workerpoolOrder = orders.workerpool; const requestOrder = orders.requester; const taskIndex = ( - await IexecAccessors__factory.connect( - this.proxyAddress, - this.accounts.anyone, - ).viewConsumed(this.hashOrder(requestOrder)) + await IexecAccessors__factory.connect(this.proxyAddress, ethers.provider).viewConsumed( + this.hashOrder(requestOrder), + ) ).toNumber(); const dealId = getDealId(this.domain, requestOrder, taskIndex); const taskId = getTaskId(dealId, taskIndex); - const volume = Number(requestOrder.volume); + const volume = ( + await IexecPocoAccessors__factory.connect( + this.proxyAddress, + ethers.provider, + ).computeDealVolume(appOrder, datasetOrder, workerpoolOrder, requestOrder) + ).toNumber(); const taskPrice = Number(appOrder.appprice) + Number(datasetOrder.datasetprice) + diff --git a/utils/poco-tools.ts b/utils/poco-tools.ts index 6b284a5c4..a076ebc78 100644 --- a/utils/poco-tools.ts +++ b/utils/poco-tools.ts @@ -83,11 +83,11 @@ export async function getIexecAccounts(): Promise { export function getDealId( domain: TypedDataDomain, requestOrder: IexecLibOrders_v5.RequestOrderStruct, - taskIndex: number = 0, + firstTaskIndex: number = 0, ): string { return ethers.utils.solidityKeccak256( ['bytes32', 'uint256'], - [hashOrder(domain, requestOrder), taskIndex], + [hashOrder(domain, requestOrder), firstTaskIndex], ); }