diff --git a/yarn-project/stdlib/src/gas/gas_fees.test.ts b/yarn-project/stdlib/src/gas/gas_fees.test.ts new file mode 100644 index 000000000000..59aff1048e84 --- /dev/null +++ b/yarn-project/stdlib/src/gas/gas_fees.test.ts @@ -0,0 +1,61 @@ +import { GasFees } from './gas_fees.js'; + +/** Helper: ceiling division for bigints, matching the fix's behavior. */ +function ceilDiv(a: bigint, b: bigint): bigint { + return (a + b - 1n) / b; +} + +describe('GasFees.mul', () => { + it('multiplies by an integer number scalar', () => { + const fees = new GasFees(100n, 200n); + const result = fees.mul(2); + expect(result).toEqual(new GasFees(200n, 400n)); + }); + + it('multiplies by a bigint scalar', () => { + const fees = new GasFees(100n, 200n); + const result = fees.mul(3n); + expect(result).toEqual(new GasFees(300n, 600n)); + }); + + it('multiplies by a non-integer scalar', () => { + const fees = new GasFees(100n, 200n); + const result = fees.mul(1.5); + expect(result).toEqual(new GasFees(150n, 300n)); + }); + + it('applies ceiling to non-integer scalar results', () => { + const fees = new GasFees(1n, 1n); + const result = fees.mul(1.5); + // 1 * 1.5 = 1.5, ceil(1.5) = 2 + expect(result).toEqual(new GasFees(2n, 2n)); + }); + + it('returns a clone when scalar is 1', () => { + const fees = new GasFees(100n, 200n); + const result = fees.mul(1); + expect(result).toEqual(fees); + expect(result).not.toBe(fees); // should be a new instance (clone) + }); + + it('preserves precision for values above 2^53 (regression test)', () => { + const bigValue = 2n ** 64n; + const fees = new GasFees(bigValue, bigValue); + const result = fees.mul(1.5); + + // 1.5 = 3/2, so the exact result is ceilDiv(2^64 * 3, 2) + const expected = ceilDiv(bigValue * 3n, 2n); + expect(result.feePerDaGas).toEqual(expected); + expect(result.feePerL2Gas).toEqual(expected); + }); + + it('preserves precision for values near 2^128', () => { + const bigValue = 2n ** 128n - 1n; // max UInt128 + const fees = new GasFees(bigValue, bigValue); + const result = fees.mul(1.5); + + const expected = ceilDiv(bigValue * 3n, 2n); + expect(result.feePerDaGas).toEqual(expected); + expect(result.feePerL2Gas).toEqual(expected); + }); +}); diff --git a/yarn-project/stdlib/src/gas/gas_fees.ts b/yarn-project/stdlib/src/gas/gas_fees.ts index 7387b2df0496..2808530499fc 100644 --- a/yarn-project/stdlib/src/gas/gas_fees.ts +++ b/yarn-project/stdlib/src/gas/gas_fees.ts @@ -15,6 +15,14 @@ import { z } from 'zod'; import type { UInt128 } from '../types/shared.js'; import type { GasDimensions } from './gas.js'; +/** Ceiling division for non-negative bigints. */ +function ceilDiv(a: bigint, b: bigint): bigint { + return (a + b - 1n) / b; +} + +/** Precision scale for converting fractional scalars to integer numerators (18 decimal digits). */ +const FRACTIONAL_PRECISION = 10n ** 18n; + /** Gas prices for each dimension. */ export class GasFees { public readonly feePerDaGas: UInt128; @@ -60,9 +68,10 @@ export class GasFees { const s = BigInt(scalar); return new GasFees(this.feePerDaGas * s, this.feePerL2Gas * s); } else { + const numerator = BigInt(Math.round(scalar * Number(FRACTIONAL_PRECISION))); return new GasFees( - BigInt(Math.ceil(Number(this.feePerDaGas) * scalar)), - BigInt(Math.ceil(Number(this.feePerL2Gas) * scalar)), + ceilDiv(this.feePerDaGas * numerator, FRACTIONAL_PRECISION), + ceilDiv(this.feePerL2Gas * numerator, FRACTIONAL_PRECISION), ); } }