Skip to content

Commit ffce9d4

Browse files
authored
Merge pull request #943 from ava-labs/fix/cap-tx-complexity
fix: cap tx complexity
2 parents f6518ea + 9a26f47 commit ffce9d4

File tree

10 files changed

+137
-17
lines changed

10 files changed

+137
-17
lines changed

src/fixtures/pvm.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ export const disableL1ValidatorTxBytes = () =>
389389
concatBytes(baseTxbytes(), idBytes(), bytesForInt(10), inputBytes());
390390

391391
export const feeState = (): FeeState => ({
392-
capacity: 1n,
392+
capacity: 999_999n,
393393
excess: 1n,
394394
price: 1n,
395395
timestamp: new Date().toISOString(),

src/vms/pvm/etna-builder/spend-reducers/fixtures/reducers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const getSpendHelper = ({
5252
> = {}) => {
5353
return new SpendHelper({
5454
changeOutputs: [],
55-
gasPrice: feeState.price,
55+
feeState,
5656
initialComplexity,
5757
inputs: [],
5858
shouldConsolidateOutputs,

src/vms/pvm/etna-builder/spend-reducers/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ export { handleFeeAndChange } from './handleFeeAndChange';
22
export { useSpendableLockedUTXOs } from './useSpendableLockedUTXOs';
33
export { useUnlockedUTXOs } from './useUnlockedUTXOs';
44
export { verifyAssetsConsumed } from './verifyAssetsConsumed';
5+
export { verifyGasUsage } from './verifyGasUsage';
56

67
export type * from './types';

src/vms/pvm/etna-builder/spend-reducers/verifyAssetsConsumed.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ describe('verifyAssetsConsumed', () => {
2222
// Mock the verifyAssetsConsumed method to throw an error
2323
// Testing for this function can be found in the spendHelper.test.ts file
2424
spendHelper.verifyAssetsConsumed = vi.fn(() => {
25-
throw new Error('Test error');
25+
return new Error('Test error');
2626
});
2727

2828
expect(() =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, test, vi } from 'vitest';
2+
import { testContext } from '../../../../fixtures/context';
3+
import { getInitialReducerState, getSpendHelper } from './fixtures/reducers';
4+
import { verifyGasUsage } from './verifyGasUsage';
5+
6+
describe('verifyGasUsage', () => {
7+
test('returns original state if gas is under the threshold', () => {
8+
const initialState = getInitialReducerState();
9+
const spendHelper = getSpendHelper();
10+
const spy = vi.spyOn(spendHelper, 'verifyGasUsage');
11+
12+
const state = verifyGasUsage(initialState, spendHelper, testContext);
13+
14+
expect(state).toBe(initialState);
15+
expect(spy).toHaveBeenCalledTimes(1);
16+
});
17+
18+
test('throws an error if gas is over the threshold', () => {
19+
const initialState = getInitialReducerState();
20+
const spendHelper = getSpendHelper();
21+
22+
// Mock the verifyGasUsage method to throw an error
23+
// Testing for this function can be found in the spendHelper.test.ts file
24+
spendHelper.verifyGasUsage = vi.fn(() => {
25+
return new Error('Test error');
26+
});
27+
28+
expect(() =>
29+
verifyGasUsage(initialState, spendHelper, testContext),
30+
).toThrow('Test error');
31+
});
32+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { SpendReducerFunction } from './types';
2+
3+
/**
4+
* Verify that gas usage is within limits.
5+
*
6+
* Calls the spendHelper's verifyGasUsage method.
7+
*/
8+
export const verifyGasUsage: SpendReducerFunction = (state, spendHelper) => {
9+
const verifyError = spendHelper.verifyGasUsage();
10+
11+
if (verifyError) {
12+
throw verifyError;
13+
}
14+
15+
return state;
16+
};

src/vms/pvm/etna-builder/spend.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Address, OutputOwners } from '../../../serializable';
44
import { createDimensions } from '../../common/fees/dimensions';
55
import {
66
verifyAssetsConsumed,
7+
verifyGasUsage,
78
type SpendReducerFunction,
89
type SpendReducerState,
910
handleFeeAndChange,
@@ -14,6 +15,7 @@ import { feeState as testFeeState } from '../../../fixtures/pvm';
1415
import { bech32ToBytes } from '../../../utils';
1516

1617
vi.mock('./spend-reducers', () => ({
18+
verifyGasUsage: vi.fn<SpendReducerFunction>((state) => state),
1719
verifyAssetsConsumed: vi.fn<SpendReducerFunction>((state) => state),
1820
handleFeeAndChange: vi.fn<SpendReducerFunction>((state) => state),
1921
}));
@@ -51,6 +53,7 @@ describe('./src/vms/pvm/etna-builder/spend.test.ts', () => {
5153

5254
expect(testReducer).toHaveBeenCalledTimes(1);
5355
expect(verifyAssetsConsumed).toHaveBeenCalledTimes(1);
56+
expect(verifyGasUsage).toHaveBeenCalledTimes(1);
5457
expect(handleFeeAndChange).toHaveBeenCalledTimes(1);
5558
});
5659

src/vms/pvm/etna-builder/spend.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import type { Dimensions } from '../../common/fees/dimensions';
99
import type { Context } from '../../context';
1010
import type { FeeState } from '../models';
1111
import type { SpendReducerFunction, SpendReducerState } from './spend-reducers';
12-
import { handleFeeAndChange, verifyAssetsConsumed } from './spend-reducers';
12+
import {
13+
handleFeeAndChange,
14+
verifyAssetsConsumed,
15+
verifyGasUsage,
16+
} from './spend-reducers';
1317
import { SpendHelper } from './spendHelper';
1418

1519
type SpendResult = Readonly<{
@@ -118,11 +122,9 @@ export const spend = (
118122
fromAddresses.map((address) => address.toBytes()),
119123
);
120124

121-
const gasPrice: bigint = feeState.price;
122-
123125
const spendHelper = new SpendHelper({
124126
changeOutputs: [],
125-
gasPrice,
127+
feeState,
126128
initialComplexity,
127129
inputs: [],
128130
shouldConsolidateOutputs,
@@ -147,6 +149,7 @@ export const spend = (
147149
...spendReducers,
148150
verifyAssetsConsumed,
149151
handleFeeAndChange,
152+
verifyGasUsage, // This should happen after change is added
150153
// Consolidation and sorting happens in the SpendHelper.
151154
];
152155

src/vms/pvm/etna-builder/spendHelper.test.ts

+35-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
import { describe, test, expect } from 'vitest';
77

88
import { id } from '../../../fixtures/common';
9-
import { stakeableLockOut } from '../../../fixtures/pvm';
9+
import type { FeeState } from '../models';
10+
import { stakeableLockOut, feeState } from '../../../fixtures/pvm';
1011
import { TransferableOutput } from '../../../serializable';
1112
import { isTransferOut } from '../../../utils';
1213
import type { Dimensions } from '../../common/fees/dimensions';
@@ -20,6 +21,7 @@ import { SpendHelper } from './spendHelper';
2021
import { getInputComplexity, getOutputComplexity } from '../txs/fee';
2122

2223
const DEFAULT_GAS_PRICE = 3n;
24+
const DEFAULT_FEE_STATE: FeeState = { ...feeState(), price: DEFAULT_GAS_PRICE };
2325

2426
const DEFAULT_WEIGHTS = createDimensions({
2527
bandwidth: 1,
@@ -30,7 +32,7 @@ const DEFAULT_WEIGHTS = createDimensions({
3032

3133
const DEFAULT_PROPS: SpendHelperProps = {
3234
changeOutputs: [],
33-
gasPrice: DEFAULT_GAS_PRICE,
35+
feeState: DEFAULT_FEE_STATE,
3436
initialComplexity: createDimensions({
3537
bandwidth: 1,
3638
dbRead: 1,
@@ -372,6 +374,37 @@ describe('src/vms/pvm/etna-builder/spendHelper', () => {
372374
);
373375
});
374376
});
377+
describe('SpendHelper.verifyGasUsage', () => {
378+
test('returns null when gas is under capacity', () => {
379+
const spendHelper = new SpendHelper({
380+
...DEFAULT_PROPS,
381+
});
382+
383+
const changeOutput = transferableOutput();
384+
385+
spendHelper.addChangeOutput(changeOutput);
386+
387+
expect(spendHelper.verifyGasUsage()).toBe(null);
388+
});
389+
390+
test('returns an error when gas is over capacity', () => {
391+
const spendHelper = new SpendHelper({
392+
...DEFAULT_PROPS,
393+
feeState: {
394+
...DEFAULT_FEE_STATE,
395+
capacity: 0n,
396+
},
397+
});
398+
399+
const changeOutput = transferableOutput();
400+
401+
spendHelper.addChangeOutput(changeOutput);
402+
403+
expect(spendHelper.verifyGasUsage()).toEqual(
404+
new Error('Gas usage of transaction (113) exceeds capacity (0)'),
405+
);
406+
});
407+
});
375408

376409
test('no consolidated outputs when `shouldConsolidateOutputs` is `false`', () => {
377410
const spendHelper = new SpendHelper(DEFAULT_PROPS);

src/vms/pvm/etna-builder/spendHelper.ts

+40-8
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import {
1111
dimensionsToGas,
1212
} from '../../common/fees/dimensions';
1313
import { consolidateOutputs } from '../../utils/consolidateOutputs';
14+
import type { FeeState } from '../models';
1415
import { getInputComplexity, getOutputComplexity } from '../txs/fee';
1516

1617
export interface SpendHelperProps {
1718
changeOutputs: readonly TransferableOutput[];
18-
gasPrice: bigint;
19+
feeState: FeeState;
1920
initialComplexity: Dimensions;
2021
inputs: readonly TransferableInput[];
2122
shouldConsolidateOutputs: boolean;
@@ -32,7 +33,7 @@ export interface SpendHelperProps {
3233
* @class
3334
*/
3435
export class SpendHelper {
35-
private readonly gasPrice: bigint;
36+
private readonly feeState: FeeState;
3637
private readonly initialComplexity: Dimensions;
3738
private readonly shouldConsolidateOutputs: boolean;
3839
private readonly toBurn: Map<string, bigint>;
@@ -47,7 +48,7 @@ export class SpendHelper {
4748

4849
constructor({
4950
changeOutputs,
50-
gasPrice,
51+
feeState,
5152
initialComplexity,
5253
inputs,
5354
shouldConsolidateOutputs,
@@ -56,7 +57,7 @@ export class SpendHelper {
5657
toStake,
5758
weights,
5859
}: SpendHelperProps) {
59-
this.gasPrice = gasPrice;
60+
this.feeState = feeState;
6061
this.initialComplexity = initialComplexity;
6162
this.shouldConsolidateOutputs = shouldConsolidateOutputs;
6263
this.toBurn = toBurn;
@@ -217,13 +218,13 @@ export class SpendHelper {
217218
}
218219

219220
/**
220-
* Calculates the fee for the SpendHelper based on its complexity and gas price.
221+
* Calculates the gas usage for the SpendHelper based on its complexity and the weights.
221222
* Provide an empty change output as a parameter to calculate the fee as if the change output was already added.
222223
*
223224
* @param {TransferableOutput} additionalOutput - The change output that has not yet been added to the SpendHelper.
224-
* @returns {bigint} The fee for the SpendHelper.
225+
* @returns {bigint} The gas usage for the SpendHelper.
225226
*/
226-
calculateFee(additionalOutput?: TransferableOutput): bigint {
227+
private calculateGas(additionalOutput?: TransferableOutput): bigint {
227228
this.consolidateOutputs();
228229

229230
const gas = dimensionsToGas(
@@ -233,7 +234,22 @@ export class SpendHelper {
233234
this.weights,
234235
);
235236

236-
return gas * this.gasPrice;
237+
return gas;
238+
}
239+
240+
/**
241+
* Calculates the fee for the SpendHelper based on its complexity and gas price.
242+
* Provide an empty change output as a parameter to calculate the fee as if the change output was already added.
243+
*
244+
* @param {TransferableOutput} additionalOutput - The change output that has not yet been added to the SpendHelper.
245+
* @returns {bigint} The fee for the SpendHelper.
246+
*/
247+
calculateFee(additionalOutput?: TransferableOutput): bigint {
248+
const gas = this.calculateGas(additionalOutput);
249+
250+
const gasPrice = this.feeState.price;
251+
252+
return gas * gasPrice;
237253
}
238254

239255
/**
@@ -281,6 +297,22 @@ export class SpendHelper {
281297
return null;
282298
}
283299

300+
/**
301+
* Verifies that gas usage does not exceed the fee state maximum.
302+
*
303+
* @returns {Error | null} An error if gas usage exceeds maximum, null otherwise.
304+
*/
305+
verifyGasUsage(): Error | null {
306+
const gas = this.calculateGas();
307+
if (this.feeState.capacity < gas) {
308+
return new Error(
309+
`Gas usage of transaction (${gas.toString()}) exceeds capacity (${this.feeState.capacity.toString()})`,
310+
);
311+
}
312+
313+
return null;
314+
}
315+
284316
/**
285317
* Gets the inputs, outputs, and UTXOs for the SpendHelper.
286318
*

0 commit comments

Comments
 (0)