Skip to content

Commit 5d4165e

Browse files
committed
feat(avm): Track gas from memory accesses explicitly
1 parent 602b3e5 commit 5d4165e

23 files changed

Lines changed: 728 additions & 390 deletions

yarn-project/foundation/src/types/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@ export type FieldsOf<T> = {
44
[P in keyof T as T[P] extends Function ? never : P]: T[P];
55
};
66

7+
/** Extracts methods of a type. */
8+
export type FunctionsOf<T> = {
9+
// eslint-disable-next-line @typescript-eslint/ban-types
10+
[P in keyof T as T[P] extends Function ? P : never]: T[P];
11+
};
12+
713
/** Marks a set of properties of a type as optional. */
814
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

yarn-project/simulator/src/avm/avm_gas.test.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,23 @@ import { encodeToBytecode } from './serialization/bytecode_serialization.js';
66

77
describe('AVM simulator: dynamic gas costs per instruction', () => {
88
it.each([
9-
[new SetInstruction(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT8, /*value=*/ 1, /*dstOffset=*/ 0), [100, 0, 0]],
10-
[new SetInstruction(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*value=*/ 1, /*dstOffset=*/ 0), [400, 0, 0]],
11-
[new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ TypeTag.UINT8, /*copySize=*/ 1, /*dstOffset=*/ 0), [10, 0, 0]],
12-
[new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ TypeTag.UINT8, /*copySize=*/ 5, /*dstOffset=*/ 0), [50, 0, 0]],
13-
[new Add(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [10, 0, 0]],
14-
[new Add(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [40, 0, 0]],
15-
[new Add(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [20, 0, 0]],
16-
[new Sub(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [20, 0, 0]],
17-
[new Mul(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [20, 0, 0]],
18-
[new Div(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [20, 0, 0]],
9+
// BASE_GAS(10) * 1 + MEMORY_WRITE(100) = 110
10+
[new SetInstruction(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT8, /*value=*/ 1, /*dstOffset=*/ 0), [110, 0, 0]],
11+
// BASE_GAS(10) * 1 + MEMORY_WRITE(100) = 110
12+
[new SetInstruction(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*value=*/ 1, /*dstOffset=*/ 0), [110]],
13+
// BASE_GAS(10) * 1 + MEMORY_WRITE(100) = 110
14+
[new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ TypeTag.UINT8, /*copySize=*/ 1, /*dstOffset=*/ 0), [110]],
15+
// BASE_GAS(10) * 5 + MEMORY_WRITE(100) * 5 = 550
16+
[new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ TypeTag.UINT8, /*copySize=*/ 5, /*dstOffset=*/ 0), [550]],
17+
// BASE_GAS(10) * 1 + MEMORY_READ(10) * 2 + MEMORY_WRITE(100) = 130
18+
[new Add(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [130]],
19+
// BASE_GAS(10) * 4 + MEMORY_READ(10) * 2 + MEMORY_WRITE(100) = 160
20+
[new Add(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [160]],
21+
// BASE_GAS(10) * 1 + MEMORY_READ(10) * 2 + MEMORY_INDIRECT_READ_PENALTY(10) * 2 + MEMORY_WRITE(100) = 150
22+
[new Add(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [150]],
23+
[new Sub(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [150]],
24+
[new Mul(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [150]],
25+
[new Div(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [150]],
1926
] as const)('computes gas cost for %s', async (instruction, [l2GasCost, l1GasCost, daGasCost]) => {
2027
const bytecode = encodeToBytecode([instruction]);
2128
const context = initContext();
@@ -27,8 +34,8 @@ describe('AVM simulator: dynamic gas costs per instruction', () => {
2734

2835
await new AvmSimulator(context).executeBytecode(bytecode);
2936

30-
expect(initialL2GasLeft - context.machineState.l2GasLeft).toEqual(l2GasCost);
31-
expect(initialL1GasLeft - context.machineState.l1GasLeft).toEqual(l1GasCost);
32-
expect(initialDaGasLeft - context.machineState.daGasLeft).toEqual(daGasCost);
37+
expect(initialL2GasLeft - context.machineState.l2GasLeft).toEqual(l2GasCost ?? 0);
38+
expect(initialL1GasLeft - context.machineState.l1GasLeft).toEqual(l1GasCost ?? 0);
39+
expect(initialDaGasLeft - context.machineState.daGasLeft).toEqual(daGasCost ?? 0);
3340
});
3441
});

yarn-project/simulator/src/avm/avm_gas.ts

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { TypeTag } from './avm_memory_types.js';
2+
import { InstructionExecutionError } from './errors.js';
23
import { Addressing, AddressingMode } from './opcodes/addressing_mode.js';
34
import { Opcode } from './serialization/instruction_serialization.js';
45

@@ -20,7 +21,7 @@ export function gasLeftToGas(gasLeft: { l1GasLeft: number; l2GasLeft: number; da
2021
}
2122

2223
/** Creates a new instance with all values set to zero except the ones set. */
23-
export function makeGasCost(gasCost: Partial<Gas>) {
24+
export function makeGas(gasCost: Partial<Gas>) {
2425
return { ...EmptyGas, ...gasCost };
2526
}
2627

@@ -36,6 +37,11 @@ export function sumGas(...gases: Partial<Gas>[]) {
3637
);
3738
}
3839

40+
/** Multiplies a gas instance by a scalar. */
41+
export function mulGas(gas: Partial<Gas>, scalar: number) {
42+
return { l1Gas: (gas.l1Gas ?? 0) * scalar, l2Gas: (gas.l2Gas ?? 0) * scalar, daGas: (gas.daGas ?? 0) * scalar };
43+
}
44+
3945
/** Zero gas across all gas dimensions. */
4046
export const EmptyGas: Gas = {
4147
l1Gas: 0,
@@ -52,12 +58,12 @@ export const DynamicGasCost = Symbol('DynamicGasCost');
5258
/** Temporary default gas cost. We should eventually remove all usage of this variable in favor of actual gas for each opcode. */
5359
const TemporaryDefaultGasCost = { l1Gas: 0, l2Gas: 10, daGas: 0 };
5460

55-
/** Gas costs for each instruction. */
56-
export const GasCosts = {
57-
[Opcode.ADD]: DynamicGasCost,
58-
[Opcode.SUB]: DynamicGasCost,
59-
[Opcode.MUL]: DynamicGasCost,
60-
[Opcode.DIV]: DynamicGasCost,
61+
/** Base gas costs for each instruction. Additional gas cost may be added on top due to memory or storage accesses, etc. */
62+
export const GasCosts: Record<Opcode, Gas | typeof DynamicGasCost> = {
63+
[Opcode.ADD]: TemporaryDefaultGasCost,
64+
[Opcode.SUB]: TemporaryDefaultGasCost,
65+
[Opcode.MUL]: TemporaryDefaultGasCost,
66+
[Opcode.DIV]: TemporaryDefaultGasCost,
6167
[Opcode.FDIV]: TemporaryDefaultGasCost,
6268
[Opcode.EQ]: TemporaryDefaultGasCost,
6369
[Opcode.LT]: TemporaryDefaultGasCost,
@@ -87,7 +93,7 @@ export const GasCosts = {
8793
[Opcode.BLOCKL1GASLIMIT]: TemporaryDefaultGasCost,
8894
[Opcode.BLOCKL2GASLIMIT]: TemporaryDefaultGasCost,
8995
[Opcode.BLOCKDAGASLIMIT]: TemporaryDefaultGasCost,
90-
[Opcode.CALLDATACOPY]: DynamicGasCost,
96+
[Opcode.CALLDATACOPY]: TemporaryDefaultGasCost,
9197
// Gas
9298
[Opcode.L1GASLEFT]: TemporaryDefaultGasCost,
9399
[Opcode.L2GASLEFT]: TemporaryDefaultGasCost,
@@ -98,7 +104,7 @@ export const GasCosts = {
98104
[Opcode.INTERNALCALL]: TemporaryDefaultGasCost,
99105
[Opcode.INTERNALRETURN]: TemporaryDefaultGasCost,
100106
// Memory
101-
[Opcode.SET]: DynamicGasCost,
107+
[Opcode.SET]: TemporaryDefaultGasCost,
102108
[Opcode.MOV]: TemporaryDefaultGasCost,
103109
[Opcode.CMOV]: TemporaryDefaultGasCost,
104110
// World state
@@ -123,35 +129,42 @@ export const GasCosts = {
123129
[Opcode.POSEIDON]: TemporaryDefaultGasCost,
124130
[Opcode.SHA256]: TemporaryDefaultGasCost, // temp - may be removed, but alot of contracts rely on i: TemporaryDefaultGasCost,
125131
[Opcode.PEDERSEN]: TemporaryDefaultGasCost, // temp - may be removed, but alot of contracts rely on i: TemporaryDefaultGasCost,t
126-
} as const;
132+
};
127133

128-
/** Returns the fixed gas cost for a given opcode, or throws if set to dynamic. */
129-
export function getFixedGasCost(opcode: Opcode): Gas {
134+
/** Returns the fixed base gas cost for a given opcode, or throws if set to dynamic. */
135+
export function getBaseGasCost(opcode: Opcode): Gas {
130136
const cost = GasCosts[opcode];
131137
if (cost === DynamicGasCost) {
132138
throw new Error(`Opcode ${Opcode[opcode]} has dynamic gas cost`);
133139
}
134140
return cost;
135141
}
136142

137-
/** Returns the additional cost from indirect accesses to memory. */
138-
export function getCostFromIndirectAccess(indirect: number): Partial<Gas> {
139-
const indirectCount = Addressing.fromWire(indirect).modePerOperand.filter(
140-
mode => mode === AddressingMode.INDIRECT,
141-
).length;
142-
return { l2Gas: indirectCount * GasCostConstants.COST_PER_INDIRECT_ACCESS };
143+
/** Returns the gas cost associated with the memory operations performed. */
144+
export function getMemoryGasCost(args: { reads?: number; writes?: number; indirect?: number }) {
145+
const { reads, writes, indirect } = args;
146+
const indirectCount = Addressing.fromWire(indirect ?? 0).count(AddressingMode.INDIRECT);
147+
const l2MemoryGasCost =
148+
(reads ?? 0) * GasCostConstants.MEMORY_READ +
149+
(writes ?? 0) * GasCostConstants.MEMORY_WRITE +
150+
indirectCount * GasCostConstants.MEMORY_INDIRECT_READ_PENALTY;
151+
return makeGas({ l2Gas: l2MemoryGasCost });
143152
}
144153

145154
/** Constants used in base cost calculations. */
146155
export const GasCostConstants = {
147-
SET_COST_PER_BYTE: 100,
148-
CALLDATACOPY_COST_PER_BYTE: 10,
149-
ARITHMETIC_COST_PER_BYTE: 10,
150-
COST_PER_INDIRECT_ACCESS: 5,
156+
MEMORY_READ: 10,
157+
MEMORY_INDIRECT_READ_PENALTY: 10,
158+
MEMORY_WRITE: 100,
151159
};
152160

161+
/** Returns gas cost for an operation on a given type tag based on the base cost per byte. */
162+
export function getGasCostForTypeTag(tag: TypeTag, baseCost: Gas) {
163+
return mulGas(baseCost, getGasCostMultiplierFromTypeTag(tag));
164+
}
165+
153166
/** Returns a multiplier based on the size of the type represented by the tag. Throws on uninitialized or invalid. */
154-
export function getGasCostMultiplierFromTypeTag(tag: TypeTag) {
167+
function getGasCostMultiplierFromTypeTag(tag: TypeTag) {
155168
switch (tag) {
156169
case TypeTag.UINT8:
157170
return 1;
@@ -167,6 +180,6 @@ export function getGasCostMultiplierFromTypeTag(tag: TypeTag) {
167180
return 32;
168181
case TypeTag.INVALID:
169182
case TypeTag.UNINITIALIZED:
170-
throw new Error(`Invalid tag type for gas cost multiplier: ${TypeTag[tag]}`);
183+
throw new InstructionExecutionError(`Invalid tag type for gas cost multiplier: ${TypeTag[tag]}`);
171184
}
172185
}

yarn-project/simulator/src/avm/avm_memory_types.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { Field, TaggedMemory, Uint8, Uint16, Uint32, Uint64, Uint128 } from './avm_memory_types.js';
1+
import {
2+
Field,
3+
MeteredTaggedMemory,
4+
TaggedMemory,
5+
Uint8,
6+
Uint16,
7+
Uint32,
8+
Uint64,
9+
Uint128,
10+
} from './avm_memory_types.js';
211

312
describe('TaggedMemory', () => {
413
it('Elements should be undefined after construction', () => {
@@ -37,6 +46,58 @@ describe('TaggedMemory', () => {
3746
});
3847
});
3948

49+
describe('MeteredTaggedMemory', () => {
50+
let mem: MeteredTaggedMemory;
51+
52+
beforeEach(() => {
53+
mem = new MeteredTaggedMemory(new TaggedMemory());
54+
});
55+
56+
it(`Counts reads`, () => {
57+
mem.get(10);
58+
mem.getAs(20);
59+
expect(mem.reset()).toEqual({ reads: 2, writes: 0 });
60+
});
61+
62+
it(`Counts reading slices`, () => {
63+
const val = [new Field(5), new Field(6), new Field(7)];
64+
mem.setSlice(10, val);
65+
mem.reset();
66+
67+
mem.getSlice(10, 3);
68+
mem.getSliceAs(11, 2);
69+
expect(mem.reset()).toEqual({ reads: 5, writes: 0 });
70+
});
71+
72+
it(`Counts writes`, () => {
73+
mem.set(10, new Uint8(5));
74+
expect(mem.reset()).toEqual({ reads: 0, writes: 1 });
75+
});
76+
77+
it(`Counts writing slices`, () => {
78+
mem.setSlice(10, [new Field(5), new Field(6)]);
79+
expect(mem.reset()).toEqual({ reads: 0, writes: 2 });
80+
});
81+
82+
it(`Clears stats`, () => {
83+
mem.get(10);
84+
mem.set(20, new Uint8(5));
85+
expect(mem.reset()).toEqual({ reads: 1, writes: 1 });
86+
expect(mem.reset()).toEqual({ reads: 0, writes: 0 });
87+
});
88+
89+
it(`Asserts stats`, () => {
90+
mem.get(10);
91+
mem.set(20, new Uint8(5));
92+
expect(() => mem.assert({ reads: 1, writes: 1 })).not.toThrow();
93+
});
94+
95+
it(`Throws on failed stat assertion`, () => {
96+
mem.get(10);
97+
expect(() => mem.assert({ reads: 1, writes: 1 })).toThrow();
98+
});
99+
});
100+
40101
type IntegralClass = typeof Uint8 | typeof Uint16 | typeof Uint32 | typeof Uint64 | typeof Uint128;
41102
describe.each([Uint8, Uint16, Uint32, Uint64, Uint128])('Integral Types', (clsValue: IntegralClass) => {
42103
describe(`${clsValue.name}`, () => {

0 commit comments

Comments
 (0)