Skip to content

Commit 7631069

Browse files
committed
fix(test-suite): harden HCU e2e tests and add build dispatch
1 parent ebe3727 commit 7631069

File tree

4 files changed

+64
-52
lines changed

4 files changed

+64
-52
lines changed

.github/workflows/test-suite-e2e-tests.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ on:
6767
description: "KMS Core version"
6868
default: ""
6969
type: string
70+
deploy-build:
71+
description: "Build local Docker images from the checked out repository before deploy"
72+
default: false
73+
type: boolean
7074
workflow_call:
7175
secrets:
7276
GHCR_READ_TOKEN:
@@ -140,7 +144,11 @@ jobs:
140144
- name: Deploy fhevm Stack
141145
working-directory: test-suite/fhevm
142146
run: |
143-
./fhevm-cli deploy
147+
if [[ "${{ inputs.deploy-build }}" == 'true' ]]; then
148+
./fhevm-cli deploy --build
149+
else
150+
./fhevm-cli deploy
151+
fi
144152
145153
# E2E tests on pausing the Host contracts
146154
- name: Pause Host Contracts

test-suite/e2e/test/encryptedERC20/EncryptedERC20.HCU.ts

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ethers } from 'hardhat';
33

44
import { createInstances } from '../instance';
55
import { getSigners, initSigners } from '../signers';
6-
import { getTxHCUFromTxReceipt, mineNBlocks } from '../utils';
6+
import { getTxHCUFromTxReceipt, mineNBlocks, waitForPendingTransactions, waitForTransactionReceipt } from '../utils';
77
import { deployEncryptedERC20Fixture } from './EncryptedERC20.fixture';
88

99
// Minimal ABI for HCULimit — the contract is deployed by the host-sc stack
@@ -105,6 +105,7 @@ describe('EncryptedERC20:HCU', function () {
105105
});
106106

107107
describe('block cap scenarios', function () {
108+
const BATCHED_TRANSFER_GAS_LIMIT = 1_000_000;
108109
let savedHCUPerBlock: bigint;
109110
let savedMaxHCUPerTx: bigint;
110111
let savedMaxHCUDepthPerTx: bigint;
@@ -168,43 +169,6 @@ describe('EncryptedERC20:HCU', function () {
168169
}
169170
});
170171

171-
it('should accumulate HCU from multiple users in the same block', async function () {
172-
await mintAndDistribute(this);
173-
await ethers.provider.send('evm_setIntervalMining', [0]);
174-
175-
// Fresh block after setup: meter should be 0
176-
await mineNBlocks(1);
177-
const [, meter0] = await this.hcuLimit.getBlockMeter();
178-
expect(meter0).to.eq(0n);
179-
180-
// Single tx (auto-mines its own block)
181-
const tx1 = await sendEncryptedTransfer(this, 'alice', this.signers.bob.address, 100);
182-
await tx1.wait();
183-
const [, meter1] = await this.hcuLimit.getBlockMeter();
184-
expect(meter1).to.be.greaterThan(meter0);
185-
186-
// Two txs batched in same block — meter must exceed the single-tx reading
187-
// Disable automine and batch both txs into one block
188-
await ethers.provider.send('evm_setAutomine', [false]);
189-
const txA = await sendEncryptedTransfer(this, 'alice', this.signers.bob.address, 100);
190-
const txB = await sendEncryptedTransfer(this, 'bob', this.signers.alice.address, 100);
191-
await ethers.provider.send('evm_mine');
192-
await ethers.provider.send('evm_setAutomine', [true]);
193-
const [receiptA, receiptB] = await Promise.all([txA.wait(), txB.wait()]);
194-
expect(receiptA?.status).to.eq(1);
195-
expect(receiptB?.status).to.eq(1);
196-
expect(receiptA?.blockNumber).to.eq(receiptB?.blockNumber);
197-
const [, meter2] = await this.hcuLimit.getBlockMeter();
198-
expect(meter2).to.be.greaterThan(meter1);
199-
200-
// Single tx in a new block — meter resets (lower than the two-tx block)
201-
const tx3 = await sendEncryptedTransfer(this, 'alice', this.signers.bob.address, 100);
202-
await tx3.wait();
203-
const [, meter3] = await this.hcuLimit.getBlockMeter();
204-
expect(meter3).to.be.greaterThan(0n);
205-
expect(meter3).to.be.lessThan(meter2);
206-
});
207-
208172
describe('with lowered limits', function () {
209173
const TIGHT_DEPTH_PER_TX = 400_000;
210174
const TIGHT_MAX_PER_TX = 600_000;
@@ -213,12 +177,12 @@ describe('EncryptedERC20:HCU', function () {
213177
beforeEach(async function () {
214178
// Narrowest-first when lowering: hcuPerBlock >= maxHCUPerTx >= maxHCUDepthPerTx
215179
const ownerHcuLimit = this.hcuLimit.connect(this.deployer);
216-
await ownerHcuLimit.setMaxHCUDepthPerTx(TIGHT_DEPTH_PER_TX);
217-
await ownerHcuLimit.setMaxHCUPerTx(TIGHT_MAX_PER_TX);
218-
await ownerHcuLimit.setHCUPerBlock(TIGHT_PER_BLOCK);
180+
await (await ownerHcuLimit.setMaxHCUDepthPerTx(TIGHT_DEPTH_PER_TX)).wait();
181+
await (await ownerHcuLimit.setMaxHCUPerTx(TIGHT_MAX_PER_TX)).wait();
182+
await (await ownerHcuLimit.setHCUPerBlock(TIGHT_PER_BLOCK)).wait();
219183
});
220184

221-
it('should revert when block HCU cap is exhausted', async function () {
185+
it('should accumulate HCU across users until the block cap is exhausted', async function () {
222186
await mintAndDistribute(this);
223187

224188
await mineNBlocks(1);
@@ -227,14 +191,19 @@ describe('EncryptedERC20:HCU', function () {
227191

228192
// Alice fills the cap, Bob would push block total over — use fixed gasLimit
229193
// to bypass estimateGas (which reverts against pending state)
230-
const tx1 = await sendEncryptedTransfer(this, 'alice', this.signers.carol.address, 100);
231-
const tx2 = await sendEncryptedTransfer(this, 'bob', this.signers.carol.address, 100, { gasLimit: 1_000_000 });
194+
const tx1 = await sendEncryptedTransfer(this, 'alice', this.signers.carol.address, 100, {
195+
gasLimit: BATCHED_TRANSFER_GAS_LIMIT,
196+
});
197+
const tx2 = await sendEncryptedTransfer(this, 'bob', this.signers.carol.address, 100, {
198+
gasLimit: BATCHED_TRANSFER_GAS_LIMIT,
199+
});
200+
await waitForPendingTransactions([tx1.hash, tx2.hash]);
232201

233202
await ethers.provider.send('evm_mine');
234203
await ethers.provider.send('evm_setAutomine', [true]);
235204
await ethers.provider.send('evm_setIntervalMining', [1]);
236205

237-
const receipt1 = await tx1.wait();
206+
const receipt1 = await waitForTransactionReceipt(tx1.hash);
238207
expect(receipt1?.status).to.eq(1, 'First transfer should succeed');
239208

240209
// Use getTransactionReceipt to avoid ethers throwing on reverted tx
@@ -251,14 +220,19 @@ describe('EncryptedERC20:HCU', function () {
251220
await ethers.provider.send('evm_setIntervalMining', [0]);
252221
await ethers.provider.send('evm_setAutomine', [false]);
253222

254-
const txAlice = await sendEncryptedTransfer(this, 'alice', this.signers.carol.address, 100);
255-
const txBob = await sendEncryptedTransfer(this, 'bob', this.signers.carol.address, 100, { gasLimit: 1_000_000 });
223+
const txAlice = await sendEncryptedTransfer(this, 'alice', this.signers.carol.address, 100, {
224+
gasLimit: BATCHED_TRANSFER_GAS_LIMIT,
225+
});
226+
const txBob = await sendEncryptedTransfer(this, 'bob', this.signers.carol.address, 100, {
227+
gasLimit: BATCHED_TRANSFER_GAS_LIMIT,
228+
});
229+
await waitForPendingTransactions([txAlice.hash, txBob.hash]);
256230

257231
await ethers.provider.send('evm_mine');
258232
await ethers.provider.send('evm_setAutomine', [true]);
259233
await ethers.provider.send('evm_setIntervalMining', [1]);
260234

261-
const receiptAlice = await txAlice.wait();
235+
const receiptAlice = await waitForTransactionReceipt(txAlice.hash);
262236
expect(receiptAlice?.status).to.eq(1, 'Alice should succeed');
263237

264238
const receiptBob = await ethers.provider.getTransactionReceipt(txBob.hash);

test-suite/e2e/test/encryptedERC20/EncryptedERC20.fixture.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ import { ethers } from 'hardhat';
22

33
import type { EncryptedERC20 } from '../../types/contracts';
44
import { getSigners } from '../signers';
5+
import { waitForTransactionReceipt } from '../utils';
56

67
export async function deployEncryptedERC20Fixture(): Promise<EncryptedERC20> {
78
const signers = await getSigners();
89

910
const contractFactory = await ethers.getContractFactory('EncryptedERC20');
10-
const contract = await contractFactory.connect(signers.alice).deploy('Naraggara', 'NARA'); // City of Zama's battle
11-
await contract.waitForDeployment();
11+
const deployTx = await contractFactory.getDeployTransaction('Naraggara', 'NARA'); // City of Zama's battle
12+
const tx = await signers.alice.sendTransaction({ ...deployTx, gasLimit: 10_000_000 });
13+
const receipt = await waitForTransactionReceipt(tx.hash);
14+
if (!receipt.contractAddress || receipt.status !== 1) {
15+
throw new Error(`EncryptedERC20 deployment failed: ${tx.hash}`);
16+
}
1217

13-
return contract;
18+
return contractFactory.attach(receipt.contractAddress) as EncryptedERC20;
1419
}

test-suite/e2e/test/utils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,31 @@ export const mineNBlocks = async (n: number) => {
8686
}
8787
};
8888

89+
export const waitForPendingTransactions = async (txHashes: string[], timeoutMs = 30_000): Promise<void> => {
90+
const start = Date.now();
91+
while (Date.now() - start < timeoutMs) {
92+
const pendingBlock = await ethers.provider.send('eth_getBlockByNumber', ['pending', false]);
93+
const pendingTxHashes = new Set<string>(pendingBlock?.transactions ?? []);
94+
if (txHashes.every((txHash) => pendingTxHashes.has(txHash))) {
95+
return;
96+
}
97+
await new Promise((resolve) => setTimeout(resolve, 50));
98+
}
99+
throw new Error(`Timed out waiting for pending txs: ${txHashes.join(', ')}`);
100+
};
101+
102+
export const waitForTransactionReceipt = async (txHash: string, timeoutMs = 30_000): Promise<TransactionReceipt> => {
103+
const start = Date.now();
104+
while (Date.now() - start < timeoutMs) {
105+
const receipt = await ethers.provider.getTransactionReceipt(txHash);
106+
if (receipt) {
107+
return receipt;
108+
}
109+
await new Promise((resolve) => setTimeout(resolve, 50));
110+
}
111+
throw new Error(`Timed out waiting for receipt: ${txHash}`);
112+
};
113+
89114
export const bigIntToBytes64 = (value: bigint) => {
90115
return new Uint8Array(toBufferBE(value, 64));
91116
};

0 commit comments

Comments
 (0)