From 6c68ce15e443b7dcdd2f314ddb4bfbd5ac3e04ef Mon Sep 17 00:00:00 2001 From: Samuel Golland Date: Tue, 17 Dec 2024 10:03:21 -0500 Subject: [PATCH 1/5] refactor: pass feestate to spendhelper --- .../spend-reducers/fixtures/reducers.ts | 2 +- src/vms/pvm/etna-builder/spend.ts | 4 +--- src/vms/pvm/etna-builder/spendHelper.test.ts | 6 ++++-- src/vms/pvm/etna-builder/spendHelper.ts | 13 ++++++++----- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/vms/pvm/etna-builder/spend-reducers/fixtures/reducers.ts b/src/vms/pvm/etna-builder/spend-reducers/fixtures/reducers.ts index 038485043..0109b483a 100644 --- a/src/vms/pvm/etna-builder/spend-reducers/fixtures/reducers.ts +++ b/src/vms/pvm/etna-builder/spend-reducers/fixtures/reducers.ts @@ -52,7 +52,7 @@ export const getSpendHelper = ({ > = {}) => { return new SpendHelper({ changeOutputs: [], - gasPrice: feeState.price, + feeState, initialComplexity, inputs: [], shouldConsolidateOutputs, diff --git a/src/vms/pvm/etna-builder/spend.ts b/src/vms/pvm/etna-builder/spend.ts index 3467640e8..77943c1fe 100644 --- a/src/vms/pvm/etna-builder/spend.ts +++ b/src/vms/pvm/etna-builder/spend.ts @@ -118,11 +118,9 @@ export const spend = ( fromAddresses.map((address) => address.toBytes()), ); - const gasPrice: bigint = feeState.price; - const spendHelper = new SpendHelper({ changeOutputs: [], - gasPrice, + feeState, initialComplexity, inputs: [], shouldConsolidateOutputs, diff --git a/src/vms/pvm/etna-builder/spendHelper.test.ts b/src/vms/pvm/etna-builder/spendHelper.test.ts index b45c6d382..5e94599a0 100644 --- a/src/vms/pvm/etna-builder/spendHelper.test.ts +++ b/src/vms/pvm/etna-builder/spendHelper.test.ts @@ -6,7 +6,8 @@ import { import { describe, test, expect } from 'vitest'; import { id } from '../../../fixtures/common'; -import { stakeableLockOut } from '../../../fixtures/pvm'; +import type { FeeState } from '../models'; +import { stakeableLockOut, feeState } from '../../../fixtures/pvm'; import { TransferableOutput } from '../../../serializable'; import { isTransferOut } from '../../../utils'; import type { Dimensions } from '../../common/fees/dimensions'; @@ -20,6 +21,7 @@ import { SpendHelper } from './spendHelper'; import { getInputComplexity, getOutputComplexity } from '../txs/fee'; const DEFAULT_GAS_PRICE = 3n; +const DEFAULT_FEE_STATE: FeeState = { ...feeState(), price: DEFAULT_GAS_PRICE }; const DEFAULT_WEIGHTS = createDimensions({ bandwidth: 1, @@ -30,7 +32,7 @@ const DEFAULT_WEIGHTS = createDimensions({ const DEFAULT_PROPS: SpendHelperProps = { changeOutputs: [], - gasPrice: DEFAULT_GAS_PRICE, + feeState: DEFAULT_FEE_STATE, initialComplexity: createDimensions({ bandwidth: 1, dbRead: 1, diff --git a/src/vms/pvm/etna-builder/spendHelper.ts b/src/vms/pvm/etna-builder/spendHelper.ts index f464a3024..bba8dcf9e 100644 --- a/src/vms/pvm/etna-builder/spendHelper.ts +++ b/src/vms/pvm/etna-builder/spendHelper.ts @@ -11,11 +11,12 @@ import { dimensionsToGas, } from '../../common/fees/dimensions'; import { consolidateOutputs } from '../../utils/consolidateOutputs'; +import type { FeeState } from '../models'; import { getInputComplexity, getOutputComplexity } from '../txs/fee'; export interface SpendHelperProps { changeOutputs: readonly TransferableOutput[]; - gasPrice: bigint; + feeState: FeeState; initialComplexity: Dimensions; inputs: readonly TransferableInput[]; shouldConsolidateOutputs: boolean; @@ -32,7 +33,7 @@ export interface SpendHelperProps { * @class */ export class SpendHelper { - private readonly gasPrice: bigint; + private readonly feeState: FeeState; private readonly initialComplexity: Dimensions; private readonly shouldConsolidateOutputs: boolean; private readonly toBurn: Map; @@ -47,7 +48,7 @@ export class SpendHelper { constructor({ changeOutputs, - gasPrice, + feeState, initialComplexity, inputs, shouldConsolidateOutputs, @@ -56,7 +57,7 @@ export class SpendHelper { toStake, weights, }: SpendHelperProps) { - this.gasPrice = gasPrice; + this.feeState = feeState; this.initialComplexity = initialComplexity; this.shouldConsolidateOutputs = shouldConsolidateOutputs; this.toBurn = toBurn; @@ -233,7 +234,9 @@ export class SpendHelper { this.weights, ); - return gas * this.gasPrice; + const gasPrice = this.feeState.price; + + return gas * gasPrice; } /** From 5030c97803e3c4dee657d4f619f6101b561fd041 Mon Sep 17 00:00:00 2001 From: Samuel Golland Date: Tue, 17 Dec 2024 11:29:36 -0500 Subject: [PATCH 2/5] chore: scaffold verify gas usage [TEMP] --- .../pvm/etna-builder/spend-reducers/index.ts | 1 + .../spend-reducers/verifyGasUsage.test.ts | 32 ++++++++++++++++++ .../spend-reducers/verifyGasUsage.ts | 19 +++++++++++ src/vms/pvm/etna-builder/spend.test.ts | 3 ++ src/vms/pvm/etna-builder/spend.ts | 3 +- src/vms/pvm/etna-builder/spendHelper.ts | 33 +++++++++++++++++-- 6 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts create mode 100644 src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.ts diff --git a/src/vms/pvm/etna-builder/spend-reducers/index.ts b/src/vms/pvm/etna-builder/spend-reducers/index.ts index e81dd077c..9b690f139 100644 --- a/src/vms/pvm/etna-builder/spend-reducers/index.ts +++ b/src/vms/pvm/etna-builder/spend-reducers/index.ts @@ -2,5 +2,6 @@ export { handleFeeAndChange } from './handleFeeAndChange'; export { useSpendableLockedUTXOs } from './useSpendableLockedUTXOs'; export { useUnlockedUTXOs } from './useUnlockedUTXOs'; export { verifyAssetsConsumed } from './verifyAssetsConsumed'; +export { verifyGasUsage } from './verifyGasUsage'; export type * from './types'; diff --git a/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts b/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts new file mode 100644 index 000000000..b957fda4e --- /dev/null +++ b/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test, vi } from 'vitest'; +import { testContext } from '../../../../fixtures/context'; +import { getInitialReducerState, getSpendHelper } from './fixtures/reducers'; +import { verifyGasUsage } from './verifyGasUsage'; + +describe('verifyGasUsage', () => { + test('returns original state if gas is under the threshold', () => { + const initialState = getInitialReducerState(); + const spendHelper = getSpendHelper(); + const spy = vi.spyOn(spendHelper, 'verifyAssetsConsumed'); + + const state = verifyAssetsConsumed(initialState, spendHelper, testContext); + + expect(state).toBe(initialState); + expect(spy).toHaveBeenCalledTimes(1); + }); + + test('throws an error if gas is over the threshold', () => { + const initialState = getInitialReducerState(); + const spendHelper = getSpendHelper(); + + // Mock the verifyAssetsConsumed method to throw an error + // Testing for this function can be found in the spendHelper.test.ts file + spendHelper.verifyAssetsConsumed = vi.fn(() => { + throw new Error('Test error'); + }); + + expect(() => + verifyAssetsConsumed(initialState, spendHelper, testContext), + ).toThrow('Test error'); + }); +}); diff --git a/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.ts b/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.ts new file mode 100644 index 000000000..d9cd8bf19 --- /dev/null +++ b/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.ts @@ -0,0 +1,19 @@ +import type { SpendReducerFunction } from './types'; + +/** + * Verify that gas usage is within limits. + * + * Calls the spendHelper's verifyGasUsage method. + */ +export const verifyGasUsage: SpendReducerFunction = ( + state, + spendHelper, +) => { + const verifyError = spendHelper.verifyGasUsage(); + + if (verifyError) { + throw verifyError; + } + + return state; +}; \ No newline at end of file diff --git a/src/vms/pvm/etna-builder/spend.test.ts b/src/vms/pvm/etna-builder/spend.test.ts index 0b093f988..5be54eed4 100644 --- a/src/vms/pvm/etna-builder/spend.test.ts +++ b/src/vms/pvm/etna-builder/spend.test.ts @@ -4,6 +4,7 @@ import { Address, OutputOwners } from '../../../serializable'; import { createDimensions } from '../../common/fees/dimensions'; import { verifyAssetsConsumed, + verifyGasUsage, type SpendReducerFunction, type SpendReducerState, handleFeeAndChange, @@ -14,6 +15,7 @@ import { feeState as testFeeState } from '../../../fixtures/pvm'; import { bech32ToBytes } from '../../../utils'; vi.mock('./spend-reducers', () => ({ + verifyGasUsage: vi.fn((state) => state), verifyAssetsConsumed: vi.fn((state) => state), handleFeeAndChange: vi.fn((state) => state), })); @@ -51,6 +53,7 @@ describe('./src/vms/pvm/etna-builder/spend.test.ts', () => { expect(testReducer).toHaveBeenCalledTimes(1); expect(verifyAssetsConsumed).toHaveBeenCalledTimes(1); + expect(verifyGasUsage).toHaveBeenCalledTimes(1); expect(handleFeeAndChange).toHaveBeenCalledTimes(1); }); diff --git a/src/vms/pvm/etna-builder/spend.ts b/src/vms/pvm/etna-builder/spend.ts index 77943c1fe..ea233d482 100644 --- a/src/vms/pvm/etna-builder/spend.ts +++ b/src/vms/pvm/etna-builder/spend.ts @@ -9,7 +9,7 @@ import type { Dimensions } from '../../common/fees/dimensions'; import type { Context } from '../../context'; import type { FeeState } from '../models'; import type { SpendReducerFunction, SpendReducerState } from './spend-reducers'; -import { handleFeeAndChange, verifyAssetsConsumed } from './spend-reducers'; +import { handleFeeAndChange, verifyAssetsConsumed, verifyGasUsage } from './spend-reducers'; import { SpendHelper } from './spendHelper'; type SpendResult = Readonly<{ @@ -145,6 +145,7 @@ export const spend = ( ...spendReducers, verifyAssetsConsumed, handleFeeAndChange, + verifyGasUsage // This should happen after change is added // Consolidation and sorting happens in the SpendHelper. ]; diff --git a/src/vms/pvm/etna-builder/spendHelper.ts b/src/vms/pvm/etna-builder/spendHelper.ts index bba8dcf9e..2c4feb42c 100644 --- a/src/vms/pvm/etna-builder/spendHelper.ts +++ b/src/vms/pvm/etna-builder/spendHelper.ts @@ -218,13 +218,13 @@ export class SpendHelper { } /** - * Calculates the fee for the SpendHelper based on its complexity and gas price. + * Calculates the gas usage for the SpendHelper based on its complexity and the weights. * Provide an empty change output as a parameter to calculate the fee as if the change output was already added. * * @param {TransferableOutput} additionalOutput - The change output that has not yet been added to the SpendHelper. - * @returns {bigint} The fee for the SpendHelper. + * @returns {bigint} The gas usage for the SpendHelper. */ - calculateFee(additionalOutput?: TransferableOutput): bigint { + private calculateGas(additionalOutput?: TransferableOutput): bigint { this.consolidateOutputs(); const gas = dimensionsToGas( @@ -234,6 +234,19 @@ export class SpendHelper { this.weights, ); + return gas; + } + + /** + * Calculates the fee for the SpendHelper based on its complexity and gas price. + * Provide an empty change output as a parameter to calculate the fee as if the change output was already added. + * + * @param {TransferableOutput} additionalOutput - The change output that has not yet been added to the SpendHelper. + * @returns {bigint} The fee for the SpendHelper. + */ + calculateFee(additionalOutput?: TransferableOutput): bigint { + const gas = this.calculateGas(additionalOutput); + const gasPrice = this.feeState.price; return gas * gasPrice; @@ -283,6 +296,20 @@ export class SpendHelper { return null; } + + /** + * Verifies that gas usage does not exceed the fee state maximum. + * + * @returns {Error | null} An error if gas usage exceeds maximum, null otherwise. + */ + verifyGasUsage(): Error | null { + const gas = this.calculateGas(); + if (this.feeState.capacity <= gas) { + return new Error(`Gas usage of transaction (${gas.toString()}) exceeds capacity (${this.feeState.capacity.toString()})`) + } + + return null; + } /** * Gets the inputs, outputs, and UTXOs for the SpendHelper. From b7ea2425f66ed0b652f5363c84e6ede341f8eb27 Mon Sep 17 00:00:00 2001 From: Samuel Golland Date: Tue, 17 Dec 2024 11:58:59 -0500 Subject: [PATCH 3/5] fix: fix tests --- src/fixtures/pvm.ts | 2 +- .../spend-reducers/verifyGasUsage.test.ts | 10 +++--- .../spend-reducers/verifyGasUsage.ts | 7 ++--- src/vms/pvm/etna-builder/spend.ts | 8 +++-- src/vms/pvm/etna-builder/spendHelper.test.ts | 31 +++++++++++++++++++ src/vms/pvm/etna-builder/spendHelper.ts | 6 ++-- 6 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/fixtures/pvm.ts b/src/fixtures/pvm.ts index 13a774d64..9d81dc60f 100644 --- a/src/fixtures/pvm.ts +++ b/src/fixtures/pvm.ts @@ -389,7 +389,7 @@ export const disableL1ValidatorTxBytes = () => concatBytes(baseTxbytes(), idBytes(), bytesForInt(10), inputBytes()); export const feeState = (): FeeState => ({ - capacity: 1n, + capacity: 1_000_000_000n, excess: 1n, price: 1n, timestamp: new Date().toISOString(), diff --git a/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts b/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts index b957fda4e..a0cf239bd 100644 --- a/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts +++ b/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts @@ -7,9 +7,9 @@ describe('verifyGasUsage', () => { test('returns original state if gas is under the threshold', () => { const initialState = getInitialReducerState(); const spendHelper = getSpendHelper(); - const spy = vi.spyOn(spendHelper, 'verifyAssetsConsumed'); + const spy = vi.spyOn(spendHelper, 'verifyGasUsage'); - const state = verifyAssetsConsumed(initialState, spendHelper, testContext); + const state = verifyGasUsage(initialState, spendHelper, testContext); expect(state).toBe(initialState); expect(spy).toHaveBeenCalledTimes(1); @@ -19,14 +19,14 @@ describe('verifyGasUsage', () => { const initialState = getInitialReducerState(); const spendHelper = getSpendHelper(); - // Mock the verifyAssetsConsumed method to throw an error + // Mock the verifyGasUsage method to throw an error // Testing for this function can be found in the spendHelper.test.ts file - spendHelper.verifyAssetsConsumed = vi.fn(() => { + spendHelper.verifyGasUsage = vi.fn(() => { throw new Error('Test error'); }); expect(() => - verifyAssetsConsumed(initialState, spendHelper, testContext), + verifyGasUsage(initialState, spendHelper, testContext), ).toThrow('Test error'); }); }); diff --git a/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.ts b/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.ts index d9cd8bf19..22370421e 100644 --- a/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.ts +++ b/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.ts @@ -5,10 +5,7 @@ import type { SpendReducerFunction } from './types'; * * Calls the spendHelper's verifyGasUsage method. */ -export const verifyGasUsage: SpendReducerFunction = ( - state, - spendHelper, -) => { +export const verifyGasUsage: SpendReducerFunction = (state, spendHelper) => { const verifyError = spendHelper.verifyGasUsage(); if (verifyError) { @@ -16,4 +13,4 @@ export const verifyGasUsage: SpendReducerFunction = ( } return state; -}; \ No newline at end of file +}; diff --git a/src/vms/pvm/etna-builder/spend.ts b/src/vms/pvm/etna-builder/spend.ts index ea233d482..ed513fdc0 100644 --- a/src/vms/pvm/etna-builder/spend.ts +++ b/src/vms/pvm/etna-builder/spend.ts @@ -9,7 +9,11 @@ import type { Dimensions } from '../../common/fees/dimensions'; import type { Context } from '../../context'; import type { FeeState } from '../models'; import type { SpendReducerFunction, SpendReducerState } from './spend-reducers'; -import { handleFeeAndChange, verifyAssetsConsumed, verifyGasUsage } from './spend-reducers'; +import { + handleFeeAndChange, + verifyAssetsConsumed, + verifyGasUsage, +} from './spend-reducers'; import { SpendHelper } from './spendHelper'; type SpendResult = Readonly<{ @@ -145,7 +149,7 @@ export const spend = ( ...spendReducers, verifyAssetsConsumed, handleFeeAndChange, - verifyGasUsage // This should happen after change is added + verifyGasUsage, // This should happen after change is added // Consolidation and sorting happens in the SpendHelper. ]; diff --git a/src/vms/pvm/etna-builder/spendHelper.test.ts b/src/vms/pvm/etna-builder/spendHelper.test.ts index 5e94599a0..65bde8ca0 100644 --- a/src/vms/pvm/etna-builder/spendHelper.test.ts +++ b/src/vms/pvm/etna-builder/spendHelper.test.ts @@ -374,6 +374,37 @@ describe('src/vms/pvm/etna-builder/spendHelper', () => { ); }); }); + describe('SpendHelper.verifyGasUsage', () => { + test('returns null when gas is under capacity', () => { + const spendHelper = new SpendHelper({ + ...DEFAULT_PROPS, + }); + + const changeOutput = transferableOutput(); + + spendHelper.addChangeOutput(changeOutput); + + expect(spendHelper.verifyGasUsage()).toBe(null); + }); + + test('returns an error when gas is over capacity', () => { + const spendHelper = new SpendHelper({ + ...DEFAULT_PROPS, + feeState: { + ...DEFAULT_FEE_STATE, + capacity: 0n, + }, + }); + + const changeOutput = transferableOutput(); + + spendHelper.addChangeOutput(changeOutput); + + expect(spendHelper.verifyGasUsage()).toEqual( + new Error('Gas usage of transaction (113) exceeds capacity (0)'), + ); + }); + }); test('no consolidated outputs when `shouldConsolidateOutputs` is `false`', () => { const spendHelper = new SpendHelper(DEFAULT_PROPS); diff --git a/src/vms/pvm/etna-builder/spendHelper.ts b/src/vms/pvm/etna-builder/spendHelper.ts index 2c4feb42c..eb396fb19 100644 --- a/src/vms/pvm/etna-builder/spendHelper.ts +++ b/src/vms/pvm/etna-builder/spendHelper.ts @@ -296,7 +296,7 @@ export class SpendHelper { return null; } - + /** * Verifies that gas usage does not exceed the fee state maximum. * @@ -305,7 +305,9 @@ export class SpendHelper { verifyGasUsage(): Error | null { const gas = this.calculateGas(); if (this.feeState.capacity <= gas) { - return new Error(`Gas usage of transaction (${gas.toString()}) exceeds capacity (${this.feeState.capacity.toString()})`) + return new Error( + `Gas usage of transaction (${gas.toString()}) exceeds capacity (${this.feeState.capacity.toString()})`, + ); } return null; From 2fb803947c7901f506ae0d316acf2ff8985a07ca Mon Sep 17 00:00:00 2001 From: Samuel Golland Date: Tue, 17 Dec 2024 12:07:51 -0500 Subject: [PATCH 4/5] fix: logic; fixture --- src/fixtures/pvm.ts | 2 +- src/vms/pvm/etna-builder/spendHelper.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fixtures/pvm.ts b/src/fixtures/pvm.ts index 9d81dc60f..59dbc1529 100644 --- a/src/fixtures/pvm.ts +++ b/src/fixtures/pvm.ts @@ -389,7 +389,7 @@ export const disableL1ValidatorTxBytes = () => concatBytes(baseTxbytes(), idBytes(), bytesForInt(10), inputBytes()); export const feeState = (): FeeState => ({ - capacity: 1_000_000_000n, + capacity: 999_999n, excess: 1n, price: 1n, timestamp: new Date().toISOString(), diff --git a/src/vms/pvm/etna-builder/spendHelper.ts b/src/vms/pvm/etna-builder/spendHelper.ts index eb396fb19..6b857124e 100644 --- a/src/vms/pvm/etna-builder/spendHelper.ts +++ b/src/vms/pvm/etna-builder/spendHelper.ts @@ -304,7 +304,7 @@ export class SpendHelper { */ verifyGasUsage(): Error | null { const gas = this.calculateGas(); - if (this.feeState.capacity <= gas) { + if (this.feeState.capacity < gas) { return new Error( `Gas usage of transaction (${gas.toString()}) exceeds capacity (${this.feeState.capacity.toString()})`, ); From 9a26f47661481ec342724f7647287985ff9e13f2 Mon Sep 17 00:00:00 2001 From: Samuel Golland Date: Tue, 17 Dec 2024 12:13:36 -0500 Subject: [PATCH 5/5] fix: tests --- .../etna-builder/spend-reducers/verifyAssetsConsumed.test.ts | 2 +- src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vms/pvm/etna-builder/spend-reducers/verifyAssetsConsumed.test.ts b/src/vms/pvm/etna-builder/spend-reducers/verifyAssetsConsumed.test.ts index d59850900..254e133a0 100644 --- a/src/vms/pvm/etna-builder/spend-reducers/verifyAssetsConsumed.test.ts +++ b/src/vms/pvm/etna-builder/spend-reducers/verifyAssetsConsumed.test.ts @@ -22,7 +22,7 @@ describe('verifyAssetsConsumed', () => { // Mock the verifyAssetsConsumed method to throw an error // Testing for this function can be found in the spendHelper.test.ts file spendHelper.verifyAssetsConsumed = vi.fn(() => { - throw new Error('Test error'); + return new Error('Test error'); }); expect(() => diff --git a/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts b/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts index a0cf239bd..01e9f2770 100644 --- a/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts +++ b/src/vms/pvm/etna-builder/spend-reducers/verifyGasUsage.test.ts @@ -22,7 +22,7 @@ describe('verifyGasUsage', () => { // Mock the verifyGasUsage method to throw an error // Testing for this function can be found in the spendHelper.test.ts file spendHelper.verifyGasUsage = vi.fn(() => { - throw new Error('Test error'); + return new Error('Test error'); }); expect(() =>