Skip to content

Commit 6d63a4d

Browse files
authored
migrate 200 IT file (#164)
2 parents 45da3e4 + faf2ef9 commit 6d63a4d

File tree

3 files changed

+305
-0
lines changed

3 files changed

+305
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- 00X_fullchain-Xworkers.js (#158, #159)
88
- 000_fullchain-5workers-1error.js (#160, #162)
99
- Clean ToDo (#163)
10+
- 200_fullchain-bot.js (#164)
1011
- Remove `smock` from unit tests:
1112
- IexecEscrow.v8 (#154, #155)
1213
- IexecPocoDelegate (#149, #151)

test/200_fullchain-bot.test.ts

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
// SPDX-FileCopyrightText: 2024 IEXEC BLOCKCHAIN TECH <[email protected]>
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers';
5+
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
6+
import { expect } from 'hardhat';
7+
import { loadHardhatFixtureDeployment } from '../scripts/hardhat-fixture-deployer';
8+
import { IexecInterfaceNative, IexecInterfaceNative__factory } from '../typechain';
9+
import { OrdersActors, OrdersAssets, OrdersPrices, buildOrders } from '../utils/createOrders';
10+
import { TaskStatusEnum, buildUtf8ResultAndDigest, getIexecAccounts } from '../utils/poco-tools';
11+
import { IexecWrapper } from './utils/IexecWrapper';
12+
13+
const standardDealTag = '0x0000000000000000000000000000000000000000000000000000000000000000';
14+
const appPrice = 1000;
15+
const datasetPrice = 1_000_000;
16+
const workerpoolPrice = 1_000_000_000;
17+
18+
let proxyAddress: string;
19+
let iexecPoco: IexecInterfaceNative;
20+
let iexecWrapper: IexecWrapper;
21+
let [appAddress, workerpoolAddress, datasetAddress]: string[] = [];
22+
let [
23+
requester,
24+
appProvider,
25+
datasetProvider,
26+
scheduler,
27+
anyone,
28+
worker1,
29+
worker2,
30+
worker3,
31+
worker4,
32+
worker5,
33+
]: SignerWithAddress[] = [];
34+
let ordersActors: OrdersActors;
35+
let ordersAssets: OrdersAssets;
36+
let ordersPrices: OrdersPrices;
37+
38+
describe('Integration tests', function () {
39+
beforeEach('Deploy', async () => {
40+
// Deploy all contracts
41+
proxyAddress = await loadHardhatFixtureDeployment();
42+
// Initialize test environment
43+
await loadFixture(initFixture);
44+
});
45+
46+
async function initFixture() {
47+
const accounts = await getIexecAccounts();
48+
({
49+
requester,
50+
appProvider,
51+
datasetProvider,
52+
scheduler,
53+
anyone,
54+
worker1,
55+
worker2,
56+
worker3,
57+
worker4,
58+
worker5,
59+
} = accounts);
60+
iexecWrapper = new IexecWrapper(proxyAddress, accounts);
61+
({ appAddress, datasetAddress, workerpoolAddress } = await iexecWrapper.createAssets());
62+
iexecPoco = IexecInterfaceNative__factory.connect(proxyAddress, anyone);
63+
ordersActors = {
64+
appOwner: appProvider,
65+
datasetOwner: datasetProvider,
66+
workerpoolOwner: scheduler,
67+
requester: requester,
68+
};
69+
ordersAssets = {
70+
app: appAddress,
71+
dataset: datasetAddress,
72+
workerpool: workerpoolAddress,
73+
};
74+
ordersPrices = {
75+
app: appPrice,
76+
dataset: datasetPrice,
77+
workerpool: workerpoolPrice,
78+
};
79+
}
80+
81+
it('Task Lifecycle with BoT Replication and Error Handling', async function () {
82+
const workers = [worker1, worker2, worker3, worker4, worker5];
83+
// Create deal.
84+
const volume = 3;
85+
const tasksAndWorkers: {
86+
[key: number]: {
87+
worker: SignerWithAddress;
88+
useEnclave: boolean;
89+
result: string;
90+
}[];
91+
} = {
92+
0: [
93+
{ worker: worker1, useEnclave: false, result: 'iExec BOT 0' },
94+
{ worker: worker2, useEnclave: false, result: 'iExec BOT 0' },
95+
],
96+
1: [
97+
{ worker: worker2, useEnclave: true, result: 'iExec BOT 1' },
98+
{ worker: worker3, useEnclave: true, result: 'iExec BOT 1' },
99+
],
100+
2: [
101+
{ worker: worker1, useEnclave: false, result: 'iExec BOT 2' },
102+
{ worker: worker3, useEnclave: false, result: '<bad contribution>' },
103+
{ worker: worker2, useEnclave: true, result: 'iExec BOT 2' },
104+
{ worker: worker4, useEnclave: true, result: 'iExec BOT 2' },
105+
{ worker: worker5, useEnclave: true, result: 'iExec BOT 2' },
106+
],
107+
};
108+
const orders = buildOrders({
109+
assets: ordersAssets,
110+
prices: ordersPrices,
111+
requester: requester.address,
112+
tag: standardDealTag,
113+
volume,
114+
trust: 4,
115+
});
116+
const { dealId, dealPrice, schedulerStakePerDeal } = await iexecWrapper.signAndMatchOrders(
117+
...orders.toArray(),
118+
);
119+
const taskPrice = appPrice + datasetPrice + workerpoolPrice;
120+
const schedulerStakePerTask = schedulerStakePerDeal / volume;
121+
// Save frozens
122+
const accounts = [requester, scheduler, appProvider, datasetProvider];
123+
const accountsInitialFrozens = await getInitialFrozens([...accounts, ...workers]);
124+
// Track initial scores
125+
// Finalize each task and check balance changes.
126+
const workerStakePerTask = await iexecPoco
127+
.viewDeal(dealId)
128+
.then((deal) => deal.workerStake.toNumber());
129+
for (let taskIndex = 0; taskIndex < volume; taskIndex++) {
130+
const taskId = await iexecWrapper.initializeTask(dealId, taskIndex);
131+
const initialScores = await getInitialScores(workers);
132+
const contributions = tasksAndWorkers[taskIndex];
133+
for (const contribution of contributions) {
134+
const { worker, useEnclave, result } = contribution;
135+
const { resultDigest } = buildUtf8ResultAndDigest(result);
136+
137+
if (useEnclave) {
138+
await iexecWrapper.contributeToTeeTask(dealId, taskIndex, resultDigest, worker);
139+
} else {
140+
await iexecWrapper.contributeToTask(dealId, taskIndex, resultDigest, worker);
141+
}
142+
}
143+
// Reveal contributions for all workers
144+
for (const contribution of contributions) {
145+
const { worker, result } = contribution;
146+
const { resultDigest } = buildUtf8ResultAndDigest(result);
147+
if (result !== '<bad contribution>') {
148+
await iexecPoco
149+
.connect(worker)
150+
.reveal(taskId, resultDigest)
151+
.then((tx) => tx.wait());
152+
}
153+
}
154+
const { results } = buildUtf8ResultAndDigest(tasksAndWorkers[taskIndex][0].result);
155+
const finalizeTx = await iexecPoco.connect(scheduler).finalize(taskId, results, '0x');
156+
await finalizeTx.wait();
157+
expect((await iexecPoco.viewTask(taskId)).status).to.equal(TaskStatusEnum.COMPLETED);
158+
// Verify token balance changes
159+
const winningWorkers = contributions
160+
.filter(
161+
(contribution) => contribution.result === 'iExec BOT ' + taskIndex.toString(),
162+
)
163+
.map((contribution) => contribution.worker);
164+
const loosingWorkers = contributions
165+
.filter(
166+
(contribution) => contribution.result !== 'iExec BOT ' + taskIndex.toString(),
167+
)
168+
.map((contribution) => contribution.worker);
169+
const nonParticipantWorkers = workers.filter(
170+
(worker) => !winningWorkers.includes(worker) && !loosingWorkers.includes(worker),
171+
);
172+
const participantWorkers = workers.filter(
173+
(worker) => !nonParticipantWorkers.includes(worker),
174+
);
175+
const totalWorkerPoolReward =
176+
workerpoolPrice + workerStakePerTask * loosingWorkers.length; // bad wrokers lose their stake and add it to the pool price
177+
// compute expected worker reward for current task
178+
const workerRewardPerTask = await computeWorkersRewardForCurrentTask(
179+
totalWorkerPoolReward,
180+
dealId,
181+
);
182+
const expectedWinningWorkerBalanceChange =
183+
workerStakePerTask + workerRewardPerTask / winningWorkers.length;
184+
// compute expected scheduler reward for current task
185+
const schedulerRewardPerTask = totalWorkerPoolReward - workerRewardPerTask;
186+
const expectedSchedulerBalanceChange = schedulerStakePerTask + schedulerRewardPerTask;
187+
188+
const expectedProxyBalanceChange = -(
189+
taskPrice +
190+
workerStakePerTask * participantWorkers.length +
191+
schedulerStakePerTask
192+
);
193+
194+
await expect(finalizeTx).to.changeTokenBalances(
195+
iexecPoco,
196+
[
197+
proxyAddress,
198+
...accounts,
199+
...winningWorkers,
200+
...loosingWorkers,
201+
...nonParticipantWorkers,
202+
],
203+
[
204+
expectedProxyBalanceChange, // Proxy
205+
-0, // Requester
206+
expectedSchedulerBalanceChange, // Scheduler
207+
appPrice, // AppProvider
208+
datasetPrice, // DatasetProvider
209+
...winningWorkers.map(() => expectedWinningWorkerBalanceChange), // winning workers
210+
...loosingWorkers.map(() => 0), // loosing workers
211+
...nonParticipantWorkers.map(() => 0), // non participant workers
212+
],
213+
);
214+
// Multiply amount by the number of finalized tasks to correctly compute
215+
// stake and reward amounts.
216+
const completedTasks = taskIndex + 1;
217+
// Calculate expected frozen changes
218+
const expectedFrozenChanges = [
219+
0, // Proxy
220+
-taskPrice * completedTasks, // Requester
221+
-schedulerStakePerTask * completedTasks, // Scheduler
222+
0, // AppProvider
223+
0, // DatasetProvider
224+
...workers.map(() => 0), // Add 0 for each worker
225+
];
226+
await checkFrozenChanges(accountsInitialFrozens, expectedFrozenChanges);
227+
await validateScores(
228+
initialScores,
229+
winningWorkers,
230+
loosingWorkers,
231+
nonParticipantWorkers,
232+
);
233+
}
234+
});
235+
async function getInitialFrozens(accounts: SignerWithAddress[]) {
236+
let initialFrozens = [
237+
{
238+
address: proxyAddress,
239+
frozen: (await iexecPoco.frozenOf(proxyAddress)).toNumber(),
240+
},
241+
];
242+
for (const account of accounts) {
243+
initialFrozens.push({
244+
address: account.address,
245+
frozen: (await iexecPoco.frozenOf(account.address)).toNumber(),
246+
});
247+
}
248+
return initialFrozens;
249+
}
250+
251+
async function getInitialScores(
252+
workers: SignerWithAddress[],
253+
): Promise<{ [address: string]: number }> {
254+
const scores: { [address: string]: number } = {};
255+
for (const worker of workers) {
256+
scores[worker.address] = (await iexecPoco.viewScore(worker.address)).toNumber();
257+
}
258+
return scores;
259+
}
260+
261+
async function computeWorkersRewardForCurrentTask(totalPoolReward: number, dealId: string) {
262+
const deal = await iexecPoco.viewDeal(dealId);
263+
return (totalPoolReward * (100 - deal.schedulerRewardRatio.toNumber())) / 100;
264+
}
265+
});
266+
267+
async function checkFrozenChanges(
268+
accountsInitialFrozens: { address: string; frozen: number }[],
269+
expectedFrozenChanges: number[],
270+
) {
271+
for (let i = 0; i < accountsInitialFrozens.length; i++) {
272+
const message = `Failed with account at index ${i}`;
273+
expect(await iexecPoco.frozenOf(accountsInitialFrozens[i].address)).to.equal(
274+
accountsInitialFrozens[i].frozen + expectedFrozenChanges[i],
275+
message,
276+
);
277+
}
278+
}
279+
280+
async function validateScores(
281+
initialScores: { [address: string]: number },
282+
winningWorkers: SignerWithAddress[],
283+
loosingWorkers: SignerWithAddress[],
284+
nonParticipantWorkers: SignerWithAddress[],
285+
) {
286+
for (const winningWorker of winningWorkers) {
287+
const currentScore = (await iexecPoco.viewScore(winningWorker.address)).toNumber();
288+
expect(currentScore, `Worker ${winningWorker.address} score mismatch`).to.equal(
289+
initialScores[winningWorker.address] + 1,
290+
);
291+
}
292+
for (const loosingWorker of loosingWorkers) {
293+
const currentScore = (await iexecPoco.viewScore(loosingWorker.address)).toNumber();
294+
expect(currentScore, `Worker ${loosingWorker.address} score mismatch`).to.equal(
295+
initialScores[loosingWorker.address] - 1,
296+
);
297+
}
298+
for (const nonParticipantWorker of nonParticipantWorkers) {
299+
const currentScore = (await iexecPoco.viewScore(nonParticipantWorker.address)).toNumber();
300+
expect(currentScore, `Worker ${nonParticipantWorker.address} score mismatch`).to.equal(
301+
initialScores[nonParticipantWorker.address],
302+
);
303+
}
304+
}

0 commit comments

Comments
 (0)