Skip to content

Commit 11699c8

Browse files
authored
feat(avm): Gas usage for nested calls (#5495)
Adds gas metering for CALLs and STATICCALLs in the AVM. Both opcodes have a fixed gas cost, an extra cost for indirect accesses to memory, and consume whatever gas they pass onto the nested call. The unused gas from the nested call gets refunded when the call returns, as specified on the yellow paper.
1 parent 1907473 commit 11699c8

14 files changed

Lines changed: 210 additions & 145 deletions

File tree

noir-projects/aztec-nr/aztec/src/context/avm_context.nr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ impl PublicContextInterface for AvmContext {
123123
temporary_function_selector: FunctionSelector,
124124
args: [Field; ARGS_COUNT]
125125
) -> [Field; RETURN_VALUES_LENGTH] {
126-
let gas = [/*l1_gas*/42, /*l2_gas*/24, /*da_gas*/420];
126+
let gas = [/*l1_gas*/10000, /*l2_gas*/10000, /*da_gas*/10000];
127127

128128
let results = call(
129129
gas,
@@ -144,7 +144,7 @@ impl PublicContextInterface for AvmContext {
144144
temporary_function_selector: FunctionSelector,
145145
args: [Field; ARGS_COUNT]
146146
) -> [Field; RETURN_VALUES_LENGTH] {
147-
let gas = [/*l1_gas*/42, /*l2_gas*/24, /*da_gas*/420];
147+
let gas = [/*l1_gas*/10000, /*l2_gas*/10000, /*da_gas*/10000];
148148

149149
let (data_to_return, success): ([Field; RETURN_VALUES_LENGTH], u8) = call_static(
150150
gas,

noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ contract AvmTest {
339339
#[aztec(public-vm)]
340340
fn raw_nested_call_to_add(arg_a: Field, arg_b: Field) -> pub Field {
341341
let selector = FunctionSelector::from_signature("add_args_return(Field,Field)").to_field();
342-
let gas = [/*l1_gas*/42, /*l2_gas*/24, /*da_gas*/420];
342+
let gas = [/*l1_gas*/10000, /*l2_gas*/10000, /*da_gas*/10000];
343343

344344
// Nested call
345345
let results = context.call_public_function_raw(gas, context.this_address(), selector, [arg_a, arg_b]);
@@ -372,7 +372,7 @@ contract AvmTest {
372372
#[aztec(public-vm)]
373373
fn raw_nested_static_call_to_add(arg_a: Field, arg_b: Field) -> pub (Field, u8) {
374374
let selector = FunctionSelector::from_signature("add_args_return(Field,Field)").to_field();
375-
let gas = [/*l1_gas*/42, /*l2_gas*/24, /*da_gas*/420];
375+
let gas = [/*l1_gas*/10000, /*l2_gas*/10000, /*da_gas*/10000];
376376

377377
let (result_data, success): ([Field; 1], u8) = context.static_call_public_function_raw(gas, context.this_address(), selector, [arg_a, arg_b]);
378378

@@ -383,7 +383,7 @@ contract AvmTest {
383383
#[aztec(public-vm)]
384384
fn raw_nested_static_call_to_set_storage() -> pub u8 {
385385
let selector = FunctionSelector::from_signature("set_storage_single(Field)").to_field();
386-
let gas = [/*l1_gas*/42, /*l2_gas*/24, /*da_gas*/420];
386+
let gas = [/*l1_gas*/10000, /*l2_gas*/10000, /*da_gas*/10000];
387387
let calldata: [Field; 1] = [20];
388388

389389
let (_data_to_return, success): ([Field; 0], u8) = context.static_call_public_function_raw(gas, context.this_address(), selector, calldata);

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ describe('Avm Context', () => {
99

1010
const newAddress = AztecAddress.random();
1111
const newCalldata = [new Fr(1), new Fr(2)];
12-
const newContext = context.createNestedContractCallContext(newAddress, newCalldata);
12+
const allocatedGas = { l1Gas: 1, l2Gas: 2, daGas: 3 }; // How much of the current call gas we pass to the nested call
13+
const newContext = context.createNestedContractCallContext(newAddress, newCalldata, allocatedGas, 'CALL');
1314

1415
expect(newContext.environment).toEqual(
1516
allSameExcept(context.environment, {
@@ -23,6 +24,9 @@ describe('Avm Context', () => {
2324
expect(newContext.machineState).toEqual(
2425
allSameExcept(context.machineState, {
2526
pc: 0,
27+
l1GasLeft: 1,
28+
l2GasLeft: 2,
29+
daGasLeft: 3,
2630
}),
2731
);
2832

@@ -36,7 +40,8 @@ describe('Avm Context', () => {
3640

3741
const newAddress = AztecAddress.random();
3842
const newCalldata = [new Fr(1), new Fr(2)];
39-
const newContext = context.createNestedContractStaticCallContext(newAddress, newCalldata);
43+
const allocatedGas = { l1Gas: 1, l2Gas: 2, daGas: 3 };
44+
const newContext = context.createNestedContractCallContext(newAddress, newCalldata, allocatedGas, 'STATICCALL');
4045

4146
expect(newContext.environment).toEqual(
4247
allSameExcept(context.environment, {
@@ -50,6 +55,9 @@ describe('Avm Context', () => {
5055
expect(newContext.machineState).toEqual(
5156
allSameExcept(context.machineState, {
5257
pc: 0,
58+
l1GasLeft: 1,
59+
l2GasLeft: 2,
60+
daGasLeft: 3,
5361
}),
5462
);
5563

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

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type AztecAddress, FunctionSelector } from '@aztec/circuits.js';
22
import { type Fr } from '@aztec/foundation/fields';
33

44
import { type AvmExecutionEnvironment } from './avm_execution_environment.js';
5+
import { type Gas, gasToGasLeft } from './avm_gas.js';
56
import { AvmMachineState } from './avm_machine_state.js';
67
import { type AvmPersistableStateManager } from './journal/journal.js';
78

@@ -33,47 +34,24 @@ export class AvmContext {
3334
*
3435
* @param address - The contract instance to initialize a context for
3536
* @param calldata - Data/arguments for nested call
37+
* @param allocatedGas - Gas allocated for the nested call
38+
* @param callType - Type of call (CALL or STATICCALL)
3639
* @returns new AvmContext instance
3740
*/
3841
public createNestedContractCallContext(
3942
address: AztecAddress,
4043
calldata: Fr[],
44+
allocatedGas: Gas,
45+
callType: 'CALL' | 'STATICCALL',
4146
temporaryFunctionSelector: FunctionSelector = FunctionSelector.empty(),
4247
): AvmContext {
43-
const newExecutionEnvironment = this.environment.deriveEnvironmentForNestedCall(
44-
address,
45-
calldata,
46-
temporaryFunctionSelector,
47-
);
48+
const deriveFn =
49+
callType === 'CALL'
50+
? this.environment.deriveEnvironmentForNestedCall
51+
: this.environment.deriveEnvironmentForNestedStaticCall;
52+
const newExecutionEnvironment = deriveFn.call(this.environment, address, calldata, temporaryFunctionSelector);
4853
const forkedWorldState = this.persistableState.fork();
49-
const machineState = AvmMachineState.fromState(this.machineState);
50-
return new AvmContext(forkedWorldState, newExecutionEnvironment, machineState);
51-
}
52-
53-
/**
54-
* Prepare a new AVM context that will be ready for an external/nested static call
55-
* - Fork the world state journal
56-
* - Derive a machine state from the current state
57-
* - E.g., gas metering is preserved but pc is reset
58-
* - Derive an execution environment from the caller/parent
59-
* - Alter both address and storageAddress
60-
*
61-
* @param address - The contract instance to initialize a context for
62-
* @param calldata - Data/arguments for nested call
63-
* @returns new AvmContext instance
64-
*/
65-
public createNestedContractStaticCallContext(
66-
address: AztecAddress,
67-
calldata: Fr[],
68-
temporaryFunctionSelector: FunctionSelector = FunctionSelector.empty(),
69-
): AvmContext {
70-
const newExecutionEnvironment = this.environment.deriveEnvironmentForNestedStaticCall(
71-
address,
72-
calldata,
73-
temporaryFunctionSelector,
74-
);
75-
const forkedWorldState = this.persistableState.fork();
76-
const machineState = AvmMachineState.fromState(this.machineState);
54+
const machineState = AvmMachineState.fromState(gasToGasLeft(allocatedGas));
7755
return new AvmContext(forkedWorldState, newExecutionEnvironment, machineState);
7856
}
7957
}
File renamed without changes.

yarn-project/simulator/src/avm/avm_gas_cost.ts renamed to yarn-project/simulator/src/avm/avm_gas.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,43 @@
11
import { TypeTag } from './avm_memory_types.js';
2+
import { Addressing, AddressingMode } from './opcodes/addressing_mode.js';
23
import { Opcode } from './serialization/instruction_serialization.js';
34

4-
/** Gas cost in L1, L2, and DA for a given instruction. */
5-
export type GasCost = {
5+
/** Gas counters in L1, L2, and DA. */
6+
export type Gas = {
67
l1Gas: number;
78
l2Gas: number;
89
daGas: number;
910
};
1011

12+
/** Maps a Gas struct to gasLeft properties. */
13+
export function gasToGasLeft(gas: Gas) {
14+
return { l1GasLeft: gas.l1Gas, l2GasLeft: gas.l2Gas, daGasLeft: gas.daGas };
15+
}
16+
17+
/** Maps gasLeft properties to a gas struct. */
18+
export function gasLeftToGas(gasLeft: { l1GasLeft: number; l2GasLeft: number; daGasLeft: number }) {
19+
return { l1Gas: gasLeft.l1GasLeft, l2Gas: gasLeft.l2GasLeft, daGas: gasLeft.daGasLeft };
20+
}
21+
1122
/** Creates a new instance with all values set to zero except the ones set. */
12-
export function makeGasCost(gasCost: Partial<GasCost>) {
13-
return { ...EmptyGasCost, ...gasCost };
23+
export function makeGasCost(gasCost: Partial<Gas>) {
24+
return { ...EmptyGas, ...gasCost };
25+
}
26+
27+
/** Sums together multiple instances of Gas. */
28+
export function sumGas(...gases: Partial<Gas>[]) {
29+
return gases.reduce(
30+
(acc: Gas, gas) => ({
31+
l1Gas: acc.l1Gas + (gas.l1Gas ?? 0),
32+
l2Gas: acc.l2Gas + (gas.l2Gas ?? 0),
33+
daGas: acc.daGas + (gas.daGas ?? 0),
34+
}),
35+
EmptyGas,
36+
);
1437
}
1538

16-
/** Gas cost of zero across all gas dimensions. */
17-
export const EmptyGasCost = {
39+
/** Zero gas across all gas dimensions. */
40+
export const EmptyGas: Gas = {
1841
l1Gas: 0,
1942
l2Gas: 0,
2043
daGas: 0,
@@ -103,12 +126,29 @@ export const GasCosts = {
103126
[Opcode.PEDERSEN]: TemporaryDefaultGasCost, // temp - may be removed, but alot of contracts rely on i: TemporaryDefaultGasCost,t
104127
} as const;
105128

129+
/** Returns the fixed gas cost for a given opcode, or throws if set to dynamic. */
130+
export function getFixedGasCost(opcode: Opcode): Gas {
131+
const cost = GasCosts[opcode];
132+
if (cost === DynamicGasCost) {
133+
throw new Error(`Opcode ${Opcode[opcode]} has dynamic gas cost`);
134+
}
135+
return cost;
136+
}
137+
138+
/** Returns the additional cost from indirect accesses to memory. */
139+
export function getCostFromIndirectAccess(indirect: number): Partial<Gas> {
140+
const indirectCount = Addressing.fromWire(indirect).modePerOperand.filter(
141+
mode => mode === AddressingMode.INDIRECT,
142+
).length;
143+
return { l2Gas: indirectCount * GasCostConstants.COST_PER_INDIRECT_ACCESS };
144+
}
145+
106146
/** Constants used in base cost calculations. */
107147
export const GasCostConstants = {
108148
SET_COST_PER_BYTE: 100,
109149
CALLDATACOPY_COST_PER_BYTE: 10,
110150
ARITHMETIC_COST_PER_BYTE: 10,
111-
ARITHMETIC_COST_PER_INDIRECT_ACCESS: 5,
151+
COST_PER_INDIRECT_ACCESS: 5,
112152
};
113153

114154
/** Returns a multiplier based on the size of the type represented by the tag. Throws on uninitialized or invalid. */

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type Fr } from '@aztec/circuits.js';
22

3-
import { type GasCost, GasDimensions } from './avm_gas_cost.js';
3+
import { type Gas, GasDimensions } from './avm_gas.js';
44
import { TaggedMemory } from './avm_memory_types.js';
55
import { AvmContractCallResults } from './avm_message_call_result.js';
66
import { OutOfGasError } from './errors.js';
@@ -59,7 +59,7 @@ export class AvmMachineState {
5959
* Should any of the gas dimensions get depleted, it sets all gas left to zero and triggers
6060
* an exceptional halt by throwing an OutOfGasError.
6161
*/
62-
public consumeGas(gasCost: Partial<GasCost>) {
62+
public consumeGas(gasCost: Partial<Gas>) {
6363
// Assert there is enough gas on every dimension.
6464
const outOfGasDimensions = GasDimensions.filter(
6565
dimension => this[`${dimension}Left`] - (gasCost[dimension] ?? 0) < 0,
@@ -76,6 +76,13 @@ export class AvmMachineState {
7676
}
7777
}
7878

79+
/** Increases the gas left by the amounts specified. */
80+
public refundGas(gasRefund: Partial<Gas>) {
81+
for (const dimension of GasDimensions) {
82+
this[`${dimension}Left`] += gasRefund[dimension] ?? 0;
83+
}
84+
}
85+
7986
/**
8087
* Most instructions just increment PC before they complete
8188
*/

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export abstract class MemoryValue {
3030
return new Fr(this.toBigInt());
3131
}
3232

33+
// To number. Throws if exceeds max safe int.
34+
public toNumber(): number {
35+
return this.toFr().toNumber();
36+
}
37+
3338
public toString(): string {
3439
return `${this.constructor.name}(0x${this.toBigInt().toString(16)})`;
3540
}

yarn-project/simulator/src/avm/opcodes/arithmetic.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import type { AvmContext } from '../avm_context.js';
2-
import { type GasCost, GasCostConstants, getGasCostMultiplierFromTypeTag, makeGasCost } from '../avm_gas_cost.js';
2+
import {
3+
type Gas,
4+
GasCostConstants,
5+
getCostFromIndirectAccess,
6+
getGasCostMultiplierFromTypeTag,
7+
sumGas,
8+
} from '../avm_gas.js';
39
import { type Field, type MemoryValue, TypeTag } from '../avm_memory_types.js';
410
import { Opcode, OperandType } from '../serialization/instruction_serialization.js';
5-
import { Addressing, AddressingMode } from './addressing_mode.js';
611
import { Instruction } from './instruction.js';
712
import { ThreeOperandInstruction } from './instruction_impl.js';
813

@@ -19,15 +24,12 @@ export abstract class ThreeOperandArithmeticInstruction extends ThreeOperandInst
1924
context.machineState.incrementPc();
2025
}
2126

22-
protected gasCost(): GasCost {
23-
const indirectCount = Addressing.fromWire(this.indirect).modePerOperand.filter(
24-
mode => mode === AddressingMode.INDIRECT,
25-
).length;
26-
27-
const l2Gas =
28-
indirectCount * GasCostConstants.ARITHMETIC_COST_PER_INDIRECT_ACCESS +
29-
getGasCostMultiplierFromTypeTag(this.inTag) * GasCostConstants.ARITHMETIC_COST_PER_BYTE;
30-
return makeGasCost({ l2Gas });
27+
protected gasCost(): Gas {
28+
const arithmeticCost = {
29+
l2Gas: getGasCostMultiplierFromTypeTag(this.inTag) * GasCostConstants.ARITHMETIC_COST_PER_BYTE,
30+
};
31+
const indirectCost = getCostFromIndirectAccess(this.indirect);
32+
return sumGas(arithmeticCost, indirectCost);
3133
}
3234

3335
protected abstract compute(a: MemoryValue, b: MemoryValue): MemoryValue;

0 commit comments

Comments
 (0)