Skip to content

Commit

Permalink
feat(avm): Dynamic gas costs for arithmetic, calldatacopy, and set (#…
Browse files Browse the repository at this point in the history
…5473)

Computes dynamic gas cost for 3-operand arithmetic instructions,
calldatacopy, and set. Base constants are arbitrary.
  • Loading branch information
spalladino authored Mar 27, 2024
1 parent ba834a4 commit bbd33fb
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 62 deletions.
34 changes: 34 additions & 0 deletions yarn-project/simulator/src/avm/avm_gas_cost.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { TypeTag } from './avm_memory_types.js';
import { AvmSimulator } from './avm_simulator.js';
import { initContext } from './fixtures/index.js';
import { Add, CalldataCopy, Div, Mul, Set as SetInstruction, Sub } from './opcodes/index.js';
import { encodeToBytecode } from './serialization/bytecode_serialization.js';

describe('AVM simulator: dynamic gas costs per instruction', () => {
it.each([
[new SetInstruction(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT8, /*value=*/ 1, /*dstOffset=*/ 0), [100, 0, 0]],
[new SetInstruction(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*value=*/ 1, /*dstOffset=*/ 0), [400, 0, 0]],
[new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ TypeTag.UINT8, /*copySize=*/ 1, /*dstOffset=*/ 0), [10, 0, 0]],
[new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ TypeTag.UINT8, /*copySize=*/ 5, /*dstOffset=*/ 0), [50, 0, 0]],
[new Add(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [10, 0, 0]],
[new Add(/*indirect=*/ 0, /*inTag=*/ TypeTag.UINT32, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [40, 0, 0]],
[new Add(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [20, 0, 0]],
[new Sub(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [20, 0, 0]],
[new Mul(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [20, 0, 0]],
[new Div(/*indirect=*/ 3, /*inTag=*/ TypeTag.UINT8, /*aOffset=*/ 1, /*bOffset=*/ 2, /*dstOffset=*/ 3), [20, 0, 0]],
] as const)('computes gas cost for %s', async (instruction, [l2GasCost, l1GasCost, daGasCost]) => {
const bytecode = encodeToBytecode([instruction]);
const context = initContext();
const {
l2GasLeft: initialL2GasLeft,
daGasLeft: initialDaGasLeft,
l1GasLeft: initialL1GasLeft,
} = context.machineState;

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

expect(initialL2GasLeft - context.machineState.l2GasLeft).toEqual(l2GasCost);
expect(initialL1GasLeft - context.machineState.l1GasLeft).toEqual(l1GasCost);
expect(initialDaGasLeft - context.machineState.daGasLeft).toEqual(daGasCost);
});
});
54 changes: 46 additions & 8 deletions yarn-project/simulator/src/avm/avm_gas_cost.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TypeTag } from './avm_memory_types.js';
import { Opcode } from './serialization/instruction_serialization.js';

/** Gas cost in L1, L2, and DA for a given instruction. */
Expand All @@ -7,25 +8,33 @@ export type GasCost = {
daGas: number;
};

/** Creates a new instance with all values set to zero except the ones set. */
export function makeGasCost(gasCost: Partial<GasCost>) {
return { ...EmptyGasCost, ...gasCost };
}

/** Gas cost of zero across all gas dimensions. */
export const EmptyGasCost = {
l1Gas: 0,
l2Gas: 0,
daGas: 0,
};

/** Dimensions of gas usage: L1, L2, and DA */
/** Dimensions of gas usage: L1, L2, and DA. */
export const GasDimensions = ['l1Gas', 'l2Gas', 'daGas'] as const;

/** Null object to represent a gas cost that's dynamic instead of fixed for a given instruction. */
export const DynamicGasCost = Symbol('DynamicGasCost');

/** Temporary default gas cost. We should eventually remove all usage of this variable in favor of actual gas for each opcode. */
const TemporaryDefaultGasCost = { l1Gas: 0, l2Gas: 10, daGas: 0 };

/** Gas costs for each instruction. */
export const GasCosts: Record<Opcode, GasCost> = {
[Opcode.ADD]: TemporaryDefaultGasCost,
[Opcode.SUB]: TemporaryDefaultGasCost,
[Opcode.MUL]: TemporaryDefaultGasCost,
[Opcode.DIV]: TemporaryDefaultGasCost,
export const GasCosts = {
[Opcode.ADD]: DynamicGasCost,
[Opcode.SUB]: DynamicGasCost,
[Opcode.MUL]: DynamicGasCost,
[Opcode.DIV]: DynamicGasCost,
[Opcode.FDIV]: TemporaryDefaultGasCost,
[Opcode.EQ]: TemporaryDefaultGasCost,
[Opcode.LT]: TemporaryDefaultGasCost,
Expand Down Expand Up @@ -55,7 +64,7 @@ export const GasCosts: Record<Opcode, GasCost> = {
[Opcode.BLOCKL1GASLIMIT]: TemporaryDefaultGasCost,
[Opcode.BLOCKL2GASLIMIT]: TemporaryDefaultGasCost,
[Opcode.BLOCKDAGASLIMIT]: TemporaryDefaultGasCost,
[Opcode.CALLDATACOPY]: TemporaryDefaultGasCost,
[Opcode.CALLDATACOPY]: DynamicGasCost,
// Gas
[Opcode.L1GASLEFT]: TemporaryDefaultGasCost,
[Opcode.L2GASLEFT]: TemporaryDefaultGasCost,
Expand All @@ -66,7 +75,7 @@ export const GasCosts: Record<Opcode, GasCost> = {
[Opcode.INTERNALCALL]: TemporaryDefaultGasCost,
[Opcode.INTERNALRETURN]: TemporaryDefaultGasCost,
// Memory
[Opcode.SET]: TemporaryDefaultGasCost,
[Opcode.SET]: DynamicGasCost,
[Opcode.MOV]: TemporaryDefaultGasCost,
[Opcode.CMOV]: TemporaryDefaultGasCost,
// World state
Expand All @@ -91,4 +100,33 @@ export const GasCosts: Record<Opcode, GasCost> = {
[Opcode.POSEIDON]: TemporaryDefaultGasCost,
[Opcode.SHA256]: TemporaryDefaultGasCost, // temp - may be removed, but alot of contracts rely on i: TemporaryDefaultGasCost,
[Opcode.PEDERSEN]: TemporaryDefaultGasCost, // temp - may be removed, but alot of contracts rely on i: TemporaryDefaultGasCost,t
} as const;

/** Constants used in base cost calculations. */
export const GasCostConstants = {
SET_COST_PER_BYTE: 100,
CALLDATACOPY_COST_PER_BYTE: 10,
ARITHMETIC_COST_PER_BYTE: 10,
ARITHMETIC_COST_PER_INDIRECT_ACCESS: 5,
};

/** Returns a multiplier based on the size of the type represented by the tag. Throws on uninitialized or invalid. */
export function getGasCostMultiplierFromTypeTag(tag: TypeTag) {
switch (tag) {
case TypeTag.UINT8:
return 1;
case TypeTag.UINT16:
return 2;
case TypeTag.UINT32:
return 4;
case TypeTag.UINT64:
return 8;
case TypeTag.UINT128:
return 16;
case TypeTag.FIELD:
return 32;
case TypeTag.INVALID:
case TypeTag.UNINITIALIZED:
throw new Error(`Invalid tag type for gas cost multiplier: ${TypeTag[tag]}`);
}
}
2 changes: 1 addition & 1 deletion yarn-project/simulator/src/avm/avm_simulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('AVM simulator: injected bytecode', () => {

expect(results.reverted).toBe(false);
expect(results.output).toEqual([new Fr(3)]);
expect(context.machineState.l2GasLeft).toEqual(initialL2GasLeft - 30);
expect(context.machineState.l2GasLeft).toEqual(initialL2GasLeft - 350);
});

it('Should halt if runs out of gas', async () => {
Expand Down
6 changes: 3 additions & 3 deletions yarn-project/simulator/src/avm/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ export function initGlobalVariables(overrides?: Partial<GlobalVariables>): Globa
*/
export function initMachineState(overrides?: Partial<AvmMachineState>): AvmMachineState {
return AvmMachineState.fromState({
l1GasLeft: overrides?.l1GasLeft ?? 1e6,
l2GasLeft: overrides?.l2GasLeft ?? 1e6,
daGasLeft: overrides?.daGasLeft ?? 1e6,
l1GasLeft: overrides?.l1GasLeft ?? 100e6,
l2GasLeft: overrides?.l2GasLeft ?? 100e6,
daGasLeft: overrides?.daGasLeft ?? 100e6,
});
}

Expand Down
2 changes: 1 addition & 1 deletion yarn-project/simulator/src/avm/opcodes/addressing_mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export enum AddressingMode {
export class Addressing {
public constructor(
/** The addressing mode for each operand. The length of this array is the number of operands of the instruction. */
private readonly modePerOperand: AddressingMode[],
public readonly modePerOperand: AddressingMode[],
) {
assert(modePerOperand.length <= 8, 'At most 8 operands are supported');
}
Expand Down
81 changes: 34 additions & 47 deletions yarn-project/simulator/src/avm/opcodes/arithmetic.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,71 @@
import type { AvmContext } from '../avm_context.js';
import { Field, TypeTag } from '../avm_memory_types.js';
import { GasCost, GasCostConstants, getGasCostMultiplierFromTypeTag, makeGasCost } from '../avm_gas_cost.js';
import { Field, MemoryValue, TypeTag } from '../avm_memory_types.js';
import { Opcode, OperandType } from '../serialization/instruction_serialization.js';
import { Addressing, AddressingMode } from './addressing_mode.js';
import { Instruction } from './instruction.js';
import { ThreeOperandInstruction } from './instruction_impl.js';

export class Add extends ThreeOperandInstruction {
static readonly type: string = 'ADD';
static readonly opcode = Opcode.ADD;

constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) {
super(indirect, inTag, aOffset, bOffset, dstOffset);
}

export abstract class ThreeOperandArithmeticInstruction extends ThreeOperandInstruction {
async execute(context: AvmContext): Promise<void> {
context.machineState.memory.checkTags(this.inTag, this.aOffset, this.bOffset);

const a = context.machineState.memory.get(this.aOffset);
const b = context.machineState.memory.get(this.bOffset);

const dest = a.add(b);
const dest = this.compute(a, b);
context.machineState.memory.set(this.dstOffset, dest);

context.machineState.incrementPc();
}
}

export class Sub extends ThreeOperandInstruction {
static readonly type: string = 'SUB';
static readonly opcode = Opcode.SUB;
protected gasCost(): GasCost {
const indirectCount = Addressing.fromWire(this.indirect).modePerOperand.filter(
mode => mode === AddressingMode.INDIRECT,
).length;

constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) {
super(indirect, inTag, aOffset, bOffset, dstOffset);
const l2Gas =
indirectCount * GasCostConstants.ARITHMETIC_COST_PER_INDIRECT_ACCESS +
getGasCostMultiplierFromTypeTag(this.inTag) * GasCostConstants.ARITHMETIC_COST_PER_BYTE;
return makeGasCost({ l2Gas });
}

async execute(context: AvmContext): Promise<void> {
const a = context.machineState.memory.get(this.aOffset);
const b = context.machineState.memory.get(this.bOffset);
protected abstract compute(a: MemoryValue, b: MemoryValue): MemoryValue;
}

const dest = a.sub(b);
context.machineState.memory.set(this.dstOffset, dest);
export class Add extends ThreeOperandArithmeticInstruction {
static readonly type: string = 'ADD';
static readonly opcode = Opcode.ADD;

context.machineState.incrementPc();
protected compute(a: MemoryValue, b: MemoryValue): MemoryValue {
return a.add(b);
}
}

export class Mul extends ThreeOperandInstruction {
static type: string = 'MUL';
static readonly opcode = Opcode.MUL;
export class Sub extends ThreeOperandArithmeticInstruction {
static readonly type: string = 'SUB';
static readonly opcode = Opcode.SUB;

constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) {
super(indirect, inTag, aOffset, bOffset, dstOffset);
protected compute(a: MemoryValue, b: MemoryValue): MemoryValue {
return a.sub(b);
}
}

async execute(context: AvmContext): Promise<void> {
const a = context.machineState.memory.get(this.aOffset);
const b = context.machineState.memory.get(this.bOffset);

const dest = a.mul(b);
context.machineState.memory.set(this.dstOffset, dest);
export class Mul extends ThreeOperandArithmeticInstruction {
static type: string = 'MUL';
static readonly opcode = Opcode.MUL;

context.machineState.incrementPc();
protected compute(a: MemoryValue, b: MemoryValue): MemoryValue {
return a.mul(b);
}
}

export class Div extends ThreeOperandInstruction {
export class Div extends ThreeOperandArithmeticInstruction {
static type: string = 'DIV';
static readonly opcode = Opcode.DIV;

constructor(indirect: number, inTag: number, aOffset: number, bOffset: number, dstOffset: number) {
super(indirect, inTag, aOffset, bOffset, dstOffset);
}

async execute(context: AvmContext): Promise<void> {
const a = context.machineState.memory.get(this.aOffset);
const b = context.machineState.memory.get(this.bOffset);

const dest = a.div(b);
context.machineState.memory.set(this.dstOffset, dest);

context.machineState.incrementPc();
protected compute(a: MemoryValue, b: MemoryValue): MemoryValue {
return a.div(b);
}
}

Expand Down
8 changes: 6 additions & 2 deletions yarn-project/simulator/src/avm/opcodes/instruction.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { strict as assert } from 'assert';

import type { AvmContext } from '../avm_context.js';
import { EmptyGasCost, GasCost, GasCosts } from '../avm_gas_cost.js';
import { DynamicGasCost, GasCost, GasCosts } from '../avm_gas_cost.js';
import { BufferCursor } from '../serialization/buffer_cursor.js';
import { Opcode, OperandType, deserialize, serialize } from '../serialization/instruction_serialization.js';

Expand Down Expand Up @@ -30,7 +30,11 @@ export abstract class Instruction {
* Instruction sub-classes can override this if their gas cost is not fixed.
*/
protected gasCost(): GasCost {
return GasCosts[this.opcode] ?? EmptyGasCost;
const gasCost = GasCosts[this.opcode];
if (gasCost === DynamicGasCost) {
throw new Error(`Instruction ${this.type} must define its own gas cost`);
}
return gasCost;
}

/**
Expand Down
9 changes: 9 additions & 0 deletions yarn-project/simulator/src/avm/opcodes/memory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AvmContext } from '../avm_context.js';
import { GasCost, GasCostConstants, getGasCostMultiplierFromTypeTag, makeGasCost } from '../avm_gas_cost.js';
import { Field, TaggedMemory, TypeTag } from '../avm_memory_types.js';
import { InstructionExecutionError } from '../errors.js';
import { BufferCursor } from '../serialization/buffer_cursor.js';
Expand Down Expand Up @@ -79,6 +80,10 @@ export class Set extends Instruction {

context.machineState.incrementPc();
}

protected gasCost(): GasCost {
return makeGasCost({ l2Gas: GasCostConstants.SET_COST_PER_BYTE * getGasCostMultiplierFromTypeTag(this.inTag) });
}
}

export class CMov extends Instruction {
Expand Down Expand Up @@ -193,4 +198,8 @@ export class CalldataCopy extends Instruction {

context.machineState.incrementPc();
}

protected gasCost(): GasCost {
return makeGasCost({ l2Gas: GasCostConstants.CALLDATACOPY_COST_PER_BYTE * this.copySize });
}
}

0 comments on commit bbd33fb

Please sign in to comment.