diff --git a/barretenberg/cpp/src/barretenberg/vm/avm/tests/execution.test.cpp b/barretenberg/cpp/src/barretenberg/vm/avm/tests/execution.test.cpp index ffb1ed18406..8ddbb487ee6 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm/tests/execution.test.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm/tests/execution.test.cpp @@ -1739,27 +1739,6 @@ TEST_F(AvmExecutionTests, daGasLeft) validate_trace(std::move(trace), public_inputs); } -TEST_F(AvmExecutionTests, ExecutorThrowsWithTooMuchGasAllocated) -{ - GTEST_SKIP(); - std::string bytecode_hex = to_hex(OpCode::GETENVVAR_16) + // opcode GETENVVAR_16(sender) - "00" // Indirect flag - + "0007" + to_hex(static_cast(EnvironmentVariable::SENDER)); // addr 7 - - std::vector calldata = {}; - std::vector returndata = {}; - public_inputs.gas_settings.gas_limits.l2_gas = MAX_L2_GAS_PER_ENQUEUED_CALL; - - auto bytecode = hex_to_bytes(bytecode_hex); - auto [instructions, error] = Deserialization::parse_bytecode_statically(bytecode); - ASSERT_TRUE(is_ok(error)); - - ExecutionHints execution_hints; - EXPECT_THROW_WITH_MESSAGE(gen_trace(bytecode, calldata, public_inputs, returndata, execution_hints), - "Cannot allocate more than MAX_L2_GAS_PER_ENQUEUED_CALL to the AVM for " - "execution of an enqueued call"); -} - // Should throw whenever the wrong number of public inputs are provided // TEST_F(AvmExecutionTests, ExecutorThrowsWithIncorrectNumberOfPublicInputs) // { diff --git a/barretenberg/cpp/src/barretenberg/vm/avm/trace/helper.hpp b/barretenberg/cpp/src/barretenberg/vm/avm/trace/helper.hpp index 0c622cfe024..727e16ec95e 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm/trace/helper.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm/trace/helper.hpp @@ -38,21 +38,6 @@ template VmPublicInputs_ convert_public_inputs(std::vector) { - if (public_inputs_vec[L2_START_GAS_LEFT_PCPI_OFFSET] > MAX_L2_GAS_PER_ENQUEUED_CALL) { - throw_or_abort( - "Cannot allocate more than MAX_L2_GAS_PER_ENQUEUED_CALL to the AVM for execution of an enqueued call"); - } - } else { - if (public_inputs_vec[L2_START_GAS_LEFT_PCPI_OFFSET].get_value() > MAX_L2_GAS_PER_ENQUEUED_CALL) { - throw_or_abort( - "Cannot allocate more than MAX_L2_GAS_PER_ENQUEUED_CALL to the AVM for execution of an enqueued call"); - } - } - std::array& kernel_inputs = std::get(public_inputs); // Copy items from PublicCircuitPublicInputs vector to public input columns diff --git a/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.cpp b/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.cpp index a370f42e5ba..8e103abe2f0 100644 --- a/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.cpp +++ b/barretenberg/cpp/src/barretenberg/vm/avm/trace/trace.cpp @@ -401,9 +401,16 @@ AvmTraceBuilder::AvmTraceBuilder(AvmPublicInputs public_inputs, , bytecode_trace_builder(execution_hints.all_contract_bytecode) , merkle_tree_trace_builder(public_inputs.start_tree_snapshots) { + // Only allocate up to the maximum L2 gas for execution + // TODO: constrain this! + auto const l2_gas_left_after_private = + public_inputs.gas_settings.gas_limits.l2_gas - public_inputs.start_gas_used.l2_gas; + // TODO: think about cast + auto const allocated_l2_gas = + std::min(l2_gas_left_after_private, static_cast(MAX_L2_GAS_PER_TX_PUBLIC_PORTION)); // TODO: think about cast gas_trace_builder.set_initial_gas( - static_cast(public_inputs.gas_settings.gas_limits.l2_gas - public_inputs.start_gas_used.l2_gas), + static_cast(allocated_l2_gas), static_cast(public_inputs.gas_settings.gas_limits.da_gas - public_inputs.start_gas_used.da_gas)); } diff --git a/barretenberg/cpp/src/barretenberg/vm/aztec_constants.hpp b/barretenberg/cpp/src/barretenberg/vm/aztec_constants.hpp index 23b1cfc86f1..04a05c5ae75 100644 --- a/barretenberg/cpp/src/barretenberg/vm/aztec_constants.hpp +++ b/barretenberg/cpp/src/barretenberg/vm/aztec_constants.hpp @@ -19,7 +19,7 @@ #define MAX_L2_TO_L1_MSGS_PER_TX 8 #define MAX_UNENCRYPTED_LOGS_PER_TX 8 #define MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS 3000 -#define MAX_L2_GAS_PER_ENQUEUED_CALL 12000000 +#define MAX_L2_GAS_PER_TX_PUBLIC_PORTION 12000000 #define CANONICAL_AUTH_REGISTRY_ADDRESS 1 #define DEPLOYER_CONTRACT_ADDRESS 2 #define REGISTERER_CONTRACT_ADDRESS 3 diff --git a/docs/docs/migration_notes.md b/docs/docs/migration_notes.md index bd8c4a622d6..2b096d8899c 100644 --- a/docs/docs/migration_notes.md +++ b/docs/docs/migration_notes.md @@ -44,6 +44,10 @@ Further changes are planned, so that: ## 0.66 +### L2 Gas limit of 12M enforced for public portion of TX + +This limit was previously enforced per-enqueued-public-call. The protocol now enforces a stricter limit that the entire public portion of a transaction consumes at most 12,000,000 L2 gas. + ### DEBUG env var is removed The `DEBUG` variable is no longer used. Use `LOG_LEVEL` with one of `silent`, `fatal`, `error`, `warn`, `info`, `verbose`, `debug`, or `trace`. To tweak log levels per module, add a list of module prefixes with their overridden level. For example, LOG_LEVEL="info; verbose: aztec:sequencer, aztec:archiver; debug: aztec:kv-store" sets `info` as the default log level, `verbose` for the sequencer and archiver, and `debug` for the kv-store. Module name match is done by prefix. diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index 79803ccb63b..0ec054d4479 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -112,12 +112,12 @@ library Constants { 14061769416655647708490531650437236735160113654556896985372298487345; uint256 internal constant DEFAULT_GAS_LIMIT = 1000000000; uint256 internal constant DEFAULT_TEARDOWN_GAS_LIMIT = 12000000; - uint256 internal constant MAX_L2_GAS_PER_ENQUEUED_CALL = 12000000; + uint256 internal constant MAX_L2_GAS_PER_TX_PUBLIC_PORTION = 12000000; uint256 internal constant DA_BYTES_PER_FIELD = 32; uint256 internal constant DA_GAS_PER_BYTE = 16; uint256 internal constant FIXED_DA_GAS = 512; uint256 internal constant FIXED_L2_GAS = 512; - uint256 internal constant FIXED_AVM_STARTUP_L2_GAS = 1024; + uint256 internal constant FIXED_AVM_STARTUP_L2_GAS = 20000; uint256 internal constant L2_GAS_DISTRIBUTED_STORAGE_PREMIUM = 1024; uint256 internal constant L2_GAS_PER_READ_MERKLE_HASH = 30; uint256 internal constant L2_GAS_PER_WRITE_MERKLE_HASH = 40; diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index f68acffd074..10a9f9cff2d 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -171,7 +171,7 @@ pub global DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_MAGIC_VALUE: Field = // GAS DEFAULTS pub global DEFAULT_GAS_LIMIT: u32 = 1_000_000_000; pub global DEFAULT_TEARDOWN_GAS_LIMIT: u32 = 12_000_000; -pub global MAX_L2_GAS_PER_ENQUEUED_CALL: u32 = 12_000_000; +pub global MAX_L2_GAS_PER_TX_PUBLIC_PORTION: u32 = 12_000_000; pub global DA_BYTES_PER_FIELD: u32 = 32; pub global DA_GAS_PER_BYTE: u32 = 16; // pays for preamble information in TX Effects @@ -179,7 +179,7 @@ pub global FIXED_DA_GAS: u32 = 512; // pays for fixed tx costs like validation, and updating state roots pub global FIXED_L2_GAS: u32 = 512; // base cost for a single public call -pub global FIXED_AVM_STARTUP_L2_GAS: u32 = 1024; +pub global FIXED_AVM_STARTUP_L2_GAS: u32 = 20_000; // Some tree insertions incur an additional cost associated with // the new database entry to be stored by all network participants. diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index 63d2e2779d2..2131eb89a96 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -98,12 +98,12 @@ export const DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_MAGIC_VALUE = 14061769416655647708490531650437236735160113654556896985372298487345n; export const DEFAULT_GAS_LIMIT = 1000000000; export const DEFAULT_TEARDOWN_GAS_LIMIT = 12000000; -export const MAX_L2_GAS_PER_ENQUEUED_CALL = 12000000; +export const MAX_L2_GAS_PER_TX_PUBLIC_PORTION = 12000000; export const DA_BYTES_PER_FIELD = 32; export const DA_GAS_PER_BYTE = 16; export const FIXED_DA_GAS = 512; export const FIXED_L2_GAS = 512; -export const FIXED_AVM_STARTUP_L2_GAS = 1024; +export const FIXED_AVM_STARTUP_L2_GAS = 20000; export const L2_GAS_DISTRIBUTED_STORAGE_PREMIUM = 1024; export const L2_GAS_PER_READ_MERKLE_HASH = 30; export const L2_GAS_PER_WRITE_MERKLE_HASH = 40; diff --git a/yarn-project/circuits.js/src/scripts/constants.in.ts b/yarn-project/circuits.js/src/scripts/constants.in.ts index 6ac65ef90ea..35fdfb9cc8c 100644 --- a/yarn-project/circuits.js/src/scripts/constants.in.ts +++ b/yarn-project/circuits.js/src/scripts/constants.in.ts @@ -82,7 +82,7 @@ const CPP_CONSTANTS = [ 'MEM_TAG_U64', 'MEM_TAG_U128', 'MEM_TAG_FF', - 'MAX_L2_GAS_PER_ENQUEUED_CALL', + 'MAX_L2_GAS_PER_TX_PUBLIC_PORTION', 'MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS', 'CANONICAL_AUTH_REGISTRY_ADDRESS', 'DEPLOYER_CONTRACT_ADDRESS', diff --git a/yarn-project/simulator/src/avm/avm_simulator.ts b/yarn-project/simulator/src/avm/avm_simulator.ts index 2f998dae352..4e5a963019c 100644 --- a/yarn-project/simulator/src/avm/avm_simulator.ts +++ b/yarn-project/simulator/src/avm/avm_simulator.ts @@ -1,4 +1,4 @@ -import { type AztecAddress, Fr, type GlobalVariables, MAX_L2_GAS_PER_ENQUEUED_CALL } from '@aztec/circuits.js'; +import { type AztecAddress, Fr, type GlobalVariables, MAX_L2_GAS_PER_TX_PUBLIC_PORTION } from '@aztec/circuits.js'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { strict as assert } from 'assert'; @@ -47,8 +47,8 @@ export class AvmSimulator { // only. Otherwise, use build() below. constructor(private context: AvmContext, private instructionSet: InstructionSet = INSTRUCTION_SET()) { assert( - context.machineState.gasLeft.l2Gas <= MAX_L2_GAS_PER_ENQUEUED_CALL, - `Cannot allocate more than ${MAX_L2_GAS_PER_ENQUEUED_CALL} to the AVM for execution of an enqueued call`, + context.machineState.gasLeft.l2Gas <= MAX_L2_GAS_PER_TX_PUBLIC_PORTION, + `Cannot allocate more than ${MAX_L2_GAS_PER_TX_PUBLIC_PORTION} to the AVM for execution.`, ); this.log = createLogger(`simulator:avm(calldata[0]: ${context.environment.calldata[0]})`); // TODO(palla/log): Should tallies be printed on debug, or only on trace? diff --git a/yarn-project/simulator/src/avm/fixtures/index.ts b/yarn-project/simulator/src/avm/fixtures/index.ts index 62080f3dd5e..ac84ef7d702 100644 --- a/yarn-project/simulator/src/avm/fixtures/index.ts +++ b/yarn-project/simulator/src/avm/fixtures/index.ts @@ -1,5 +1,5 @@ import { isNoirCallStackUnresolved } from '@aztec/circuit-types'; -import { GasFees, GlobalVariables, MAX_L2_GAS_PER_ENQUEUED_CALL } from '@aztec/circuits.js'; +import { GasFees, GlobalVariables, MAX_L2_GAS_PER_TX_PUBLIC_PORTION } from '@aztec/circuits.js'; import { type FunctionArtifact, FunctionSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -93,7 +93,7 @@ export function initGlobalVariables(overrides?: Partial): Globa */ export function initMachineState(overrides?: Partial): AvmMachineState { return AvmMachineState.fromState({ - l2GasLeft: overrides?.l2GasLeft ?? MAX_L2_GAS_PER_ENQUEUED_CALL, + l2GasLeft: overrides?.l2GasLeft ?? MAX_L2_GAS_PER_TX_PUBLIC_PORTION, daGasLeft: overrides?.daGasLeft ?? 1e8, }); } diff --git a/yarn-project/simulator/src/public/fixtures/index.ts b/yarn-project/simulator/src/public/fixtures/index.ts index acbefc4be15..bf4940feb8e 100644 --- a/yarn-project/simulator/src/public/fixtures/index.ts +++ b/yarn-project/simulator/src/public/fixtures/index.ts @@ -12,7 +12,7 @@ import { GasFees, GasSettings, GlobalVariables, - MAX_L2_GAS_PER_ENQUEUED_CALL, + MAX_L2_GAS_PER_TX_PUBLIC_PORTION, PartialPrivateTailPublicInputsForPublic, PrivateKernelTailCircuitPublicInputs, type PublicFunction, @@ -113,7 +113,7 @@ export function createTxForPublicCall( ): Tx { const callRequest = executionRequest.toCallRequest(); // use max limits - const gasLimits = new Gas(DEFAULT_GAS_LIMIT, MAX_L2_GAS_PER_ENQUEUED_CALL); + const gasLimits = new Gas(DEFAULT_GAS_LIMIT, MAX_L2_GAS_PER_TX_PUBLIC_PORTION); const forPublic = PartialPrivateTailPublicInputsForPublic.empty(); // TODO(#9269): Remove this fake nullifier method as we move away from 1st nullifier as hash. diff --git a/yarn-project/simulator/src/public/public_tx_context.ts b/yarn-project/simulator/src/public/public_tx_context.ts index 23d0a14bd53..3971f0b73fe 100644 --- a/yarn-project/simulator/src/public/public_tx_context.ts +++ b/yarn-project/simulator/src/public/public_tx_context.ts @@ -17,6 +17,7 @@ import { Gas, type GasSettings, type GlobalVariables, + MAX_L2_GAS_PER_TX_PUBLIC_PORTION, type PrivateToPublicAccumulatedData, type PublicCallRequest, PublicCircuitPublicInputs, @@ -43,7 +44,7 @@ export class PublicTxContext { private log: Logger; /* Gas used including private, teardown gas _limit_, setup and app logic */ - private gasUsed: Gas; + private gasUsedByPublic: Gas = Gas.empty(); /* Gas actually used during teardown (different from limit) */ public teardownGasUsed: Gas = Gas.empty(); @@ -60,8 +61,9 @@ export class PublicTxContext { public readonly state: PhaseStateManager, private readonly globalVariables: GlobalVariables, private readonly startStateReference: StateReference, - private readonly startGasUsed: Gas, private readonly gasSettings: GasSettings, + private readonly gasUsedByPrivate: Gas, + private readonly gasAllocatedToPublic: Gas, private readonly setupCallRequests: PublicCallRequest[], private readonly appLogicCallRequests: PublicCallRequest[], private readonly teardownCallRequests: PublicCallRequest[], @@ -73,7 +75,6 @@ export class PublicTxContext { public trace: PublicEnqueuedCallSideEffectTrace, // FIXME(dbanks12): should be private ) { this.log = createLogger(`simulator:public_tx_context`); - this.gasUsed = startGasUsed; } public static async create( @@ -100,12 +101,18 @@ export class PublicTxContext { // Transaction level state manager that will be forked for revertible phases. const txStateManager = await AvmPersistableStateManager.create(worldStateDB, enqueuedCallTrace, doMerkleOperations); + const gasSettings = tx.data.constants.txContext.gasSettings; + const gasUsedByPrivate = tx.data.gasUsed; + // Gas allocated to public is "whatever's left" after private, but with some max applied. + const gasAllocatedToPublic = applyMaxToAvailableGas(gasSettings.gasLimits.sub(gasUsedByPrivate)); + return new PublicTxContext( new PhaseStateManager(txStateManager), globalVariables, await db.getStateReference(), - tx.data.gasUsed, - tx.data.constants.txContext.gasSettings, + gasSettings, + gasUsedByPrivate, + gasAllocatedToPublic, getCallRequestsByPhase(tx, TxExecutionPhase.SETUP), getCallRequestsByPhase(tx, TxExecutionPhase.APP_LOGIC), getCallRequestsByPhase(tx, TxExecutionPhase.TEARDOWN), @@ -226,13 +233,14 @@ export class PublicTxContext { } /** - * How much gas is left for the specified phase? + * How much gas is left as of the specified phase? */ - getGasLeftForPhase(phase: TxExecutionPhase): Gas { + getGasLeftAtPhase(phase: TxExecutionPhase): Gas { if (phase === TxExecutionPhase.TEARDOWN) { - return this.gasSettings.teardownGasLimits; + return applyMaxToAvailableGas(this.gasSettings.teardownGasLimits); } else { - return this.gasSettings.gasLimits.sub(this.gasUsed); + const gasLeftForPublic = this.gasAllocatedToPublic.sub(this.gasUsedByPublic); + return gasLeftForPublic; } } @@ -243,10 +251,18 @@ export class PublicTxContext { if (phase === TxExecutionPhase.TEARDOWN) { this.teardownGasUsed = this.teardownGasUsed.add(gas); } else { - this.gasUsed = this.gasUsed.add(gas); + this.gasUsedByPublic = this.gasUsedByPublic.add(gas); } } + /** + * The gasUsed by public and private, + * as if the entire teardown gas limit was consumed. + */ + getTotalGasUsed(): Gas { + return this.gasUsedByPrivate.add(this.gasUsedByPublic); + } + /** * Compute the gas used using the actual gas used during teardown instead * of the teardown gas limit. @@ -257,14 +273,7 @@ export class PublicTxContext { assert(this.halted, 'Can only compute actual gas used after tx execution ends'); const requireTeardown = this.teardownCallRequests.length > 0; const teardownGasLimits = requireTeardown ? this.gasSettings.teardownGasLimits : Gas.empty(); - return this.gasUsed.sub(teardownGasLimits).add(this.teardownGasUsed); - } - - /** - * The gasUsed as if the entire teardown gas limit was consumed. - */ - getGasUsedForFee(): Gas { - return this.gasUsed; + return this.getTotalGasUsed().sub(teardownGasLimits).add(this.teardownGasUsed); } /** @@ -284,10 +293,10 @@ export class PublicTxContext { * Should only be called during or after teardown. */ private getTransactionFeeUnsafe(): Fr { - const txFee = this.gasUsed.computeFee(this.globalVariables.gasFees); + const txFee = this.getTotalGasUsed().computeFee(this.globalVariables.gasFees); this.log.debug(`Computed tx fee`, { txFee, - gasUsed: inspect(this.gasUsed), + gasUsed: inspect(this.getTotalGasUsed()), gasFees: inspect(this.globalVariables.gasFees), }); return txFee; @@ -320,7 +329,7 @@ export class PublicTxContext { this.trace, this.globalVariables, this.startStateReference, - this.startGasUsed, + /*startGasUsed=*/ this.gasUsedByPrivate, this.gasSettings, this.setupCallRequests, this.appLogicCallRequests, @@ -328,7 +337,7 @@ export class PublicTxContext { this.nonRevertibleAccumulatedDataFromPrivate, this.revertibleAccumulatedDataFromPrivate, endTreeSnapshots, - /*endGasUsed=*/ this.gasUsed, + /*endGasUsed=*/ this.getTotalGasUsed(), this.getTransactionFeeUnsafe(), this.revertCode, ); @@ -401,3 +410,13 @@ class PhaseStateManager { this.currentlyActiveStateManager = undefined; } } + +/** + * Apply L2 gas maximum. + */ +function applyMaxToAvailableGas(availableGas: Gas) { + return new Gas( + /*daGas=*/ availableGas.daGas, + /*l2Gas=*/ Math.min(availableGas.l2Gas, MAX_L2_GAS_PER_TX_PUBLIC_PORTION), + ); +} diff --git a/yarn-project/simulator/src/public/public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator.ts index b77ff48ae7d..d7c6aecc7cc 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator.ts @@ -10,14 +10,7 @@ import { UnencryptedFunctionL2Logs, } from '@aztec/circuit-types'; import { type AvmSimulationStats } from '@aztec/circuit-types/stats'; -import { - type Fr, - Gas, - type GlobalVariables, - MAX_L2_GAS_PER_ENQUEUED_CALL, - type PublicCallRequest, - type RevertCode, -} from '@aztec/circuits.js'; +import { type Fr, type Gas, type GlobalVariables, type PublicCallRequest, type RevertCode } from '@aztec/circuits.js'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { Timer } from '@aztec/foundation/timer'; import { Attributes, type TelemetryClient, type Tracer, trackSpan } from '@aztec/telemetry-client'; @@ -266,23 +259,17 @@ export class PublicTxSimulator { const address = executionRequest.callContext.contractAddress; const fnName = await getPublicFunctionDebugName(this.worldStateDB, address, executionRequest.args); - const availableGas = context.getGasLeftForPhase(phase); - // Gas allocated to an enqueued call can be different from the available gas - // if there is more gas available than the max allocation per enqueued call. - const allocatedGas = new Gas( - /*daGas=*/ availableGas.daGas, - /*l2Gas=*/ Math.min(availableGas.l2Gas, MAX_L2_GAS_PER_ENQUEUED_CALL), - ); + const allocatedGas = context.getGasLeftAtPhase(phase); const result = await this.simulateEnqueuedCallInternal( context.state.getActiveStateManager(), executionRequest, allocatedGas, - context.getTransactionFee(phase), + /*transactionFee=*/ context.getTransactionFee(phase), fnName, ); - const gasUsed = allocatedGas.sub(result.gasLeft); + const gasUsed = allocatedGas.sub(result.gasLeft); // by enqueued call context.consumeGas(phase, gasUsed); this.log.verbose( `[AVM] Enqueued public call consumed ${gasUsed.l2Gas} L2 gas ending with ${result.gasLeft.l2Gas} L2 gas left.`,