Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace structured clone with deep copy in fromProto in Quai tx #375

Prev Previous commit
Next Next commit
check tx gas against block gas limit
alejoacosta74 committed Dec 18, 2024

Verified

This commit was signed with the committer’s verified signature.
commit 3fb95d62e21ad3605034330827d0ab55b052d556
126 changes: 126 additions & 0 deletions src/_tests/unit/qihdwallet-check-gas-limit.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import assert from 'assert';
import { QiHDWallet } from '../../wallet/qi-hdwallet.js';
import { Mnemonic } from '../../wallet/mnemonic.js';
import { Zone } from '../../constants/zones.js';
import { MockProvider } from './mockProvider.js';
import { QiTransaction } from '../../transaction/index.js';
import { Block } from '../../providers/index.js';

class TestQiHDWallet extends QiHDWallet {
public async checkGasLimit(tx: QiTransaction, zone: Zone): Promise<boolean> {
return this['_verifyGasLimit'](tx, zone);
}
}

interface GasLimitTestCase {
name: string;
mnemonic: string;
zone: Zone;
blockGasLimit: bigint;
estimatedGas: bigint;
expectedResult: boolean;
}

const testMnemonic = 'test test test test test test test test test test test junk';

describe('QiHDWallet: Gas Limit Tests', () => {
const testCases: GasLimitTestCase[] = [
{
name: 'Gas limit is sufficient (well below 90%)',
mnemonic: testMnemonic,
zone: Zone.Cyprus1,
blockGasLimit: BigInt(30000),
estimatedGas: BigInt(21000), // 70% of block gas limit
expectedResult: true,
},
{
name: 'Gas limit is insufficient (above 90%)',
mnemonic: testMnemonic,
zone: Zone.Cyprus1,
blockGasLimit: BigInt(20000),
estimatedGas: BigInt(19000), // 95% of block gas limit
expectedResult: false,
},
{
name: 'Gas limit exactly at 90%',
mnemonic: testMnemonic,
zone: Zone.Cyprus1,
blockGasLimit: BigInt(20000),
estimatedGas: BigInt(18000), // exactly 90% of block gas limit
expectedResult: true,
},
{
name: 'Gas limit slightly below 90%',
mnemonic: testMnemonic,
zone: Zone.Cyprus1,
blockGasLimit: BigInt(20000),
estimatedGas: BigInt(17900), // 89.5% of block gas limit
expectedResult: true,
},
{
name: 'Gas limit slightly above 90%',
mnemonic: testMnemonic,
zone: Zone.Cyprus1,
blockGasLimit: BigInt(20000),
estimatedGas: BigInt(18100), // 90.5% of block gas limit
expectedResult: false,
},
];

testCases.forEach((testCase) => {
it(testCase.name, async () => {
const mnemonic = Mnemonic.fromPhrase(testCase.mnemonic);
const wallet = TestQiHDWallet.fromMnemonic(mnemonic);

const mockProvider = new MockProvider({ network: BigInt(1) });

mockProvider.getBlock = async () => {
return {
header: {
gasLimit: testCase.blockGasLimit,
},
} as Block;
};

mockProvider.estimateGas = async () => {
return testCase.estimatedGas;
};

wallet.connect(mockProvider);

const tx = new QiTransaction();

const result = await wallet.checkGasLimit(tx, testCase.zone);
assert.equal(
result,
testCase.expectedResult,
`Expected gas limit check to return ${testCase.expectedResult} but got ${result}`,
);
});
});

it('should throw error when provider is not set', async () => {
const mnemonic = Mnemonic.fromPhrase(testMnemonic);
const wallet = TestQiHDWallet.fromMnemonic(mnemonic);
const tx = new QiTransaction();

await assert.rejects(async () => await wallet.checkGasLimit(tx, Zone.Cyprus1), {
message: 'Provider is not set',
});
});

it('should throw error when block cannot be retrieved', async () => {
const mnemonic = Mnemonic.fromPhrase(testMnemonic);
const wallet = TestQiHDWallet.fromMnemonic(mnemonic);
const mockProvider = new MockProvider({ network: BigInt(1) });

mockProvider.getBlock = async () => null;

wallet.connect(mockProvider);
const tx = new QiTransaction();

await assert.rejects(async () => await wallet.checkGasLimit(tx, Zone.Cyprus1), {
message: 'Failed to get the current block',
});
});
});
36 changes: 36 additions & 0 deletions src/wallet/qi-hdwallet.ts
Original file line number Diff line number Diff line change
@@ -715,6 +715,11 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {
Number(chainId),
);

// verify tx gas is under block gas limit
if (!(await this._verifyGasLimit(tx, zone))) {
throw new Error('Transaction gas limit exceeds block gas limit');
}

// Sign the transaction
const signedTx = await this.signTransaction(tx);

@@ -895,6 +900,11 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {
Number(chainId),
);

// verify tx gas is under block gas limit
if (!(await this._verifyGasLimit(tx, originZone))) {
throw new Error('Transaction gas limit exceeds block gas limit');
}

// Sign the transaction
const signedTx = await this.signTransaction(tx);
// Broadcast the transaction to the network using the provider
@@ -989,6 +999,32 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {
txOut,
};
}
/**
* Checks if the estimated gas for a transaction is within the current block's gas limit.
*
* @private
* @param {QiTransaction} tx - The Qi transaction to check
* @param {Zone} zone - The zone where the transaction will be executed
* @returns {Promise<boolean>} Returns true if the estimated gas is within block limit, false otherwise
* @throws {Error} If provider is not set or block cannot be retrieved
*/
private async _verifyGasLimit(tx: QiTransaction, zone: Zone): Promise<boolean> {
if (!this.provider) {
throw new Error('Provider is not set');
}
const currentBlock = await this.provider.getBlock(toShard(zone), 'latest')!;
if (!currentBlock) {
throw new Error('Failed to get the current block');
}

const blockGasLimit = currentBlock.header.gasLimit;

const txEstimatedGas = await this.provider.estimateGas(tx);

const blockGasLimitThreshold = (blockGasLimit * 9n) / 10n; // 90% of blockGasLimit

return txEstimatedGas <= blockGasLimitThreshold;
}

/**
* Gets a set of unused BIP44 addresses from the specified derivation path. It first checks if there are any unused