Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(avm): Dynamic gas costs for arithmetic, calldatacopy, and set #5473

Merged
merged 2 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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 });
}
}
Loading