-
Notifications
You must be signed in to change notification settings - Fork 310
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): Track gas usage in AVM simulator #5438
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { Opcode } from './serialization/instruction_serialization.js'; | ||
|
||
/** Gas cost in L1, L2, and DA for a given instruction. */ | ||
export type GasCost = { | ||
l1Gas: number; | ||
l2Gas: number; | ||
daGas: number; | ||
}; | ||
|
||
/** 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 */ | ||
export const GasDimensions = ['l1Gas', 'l2Gas', 'daGas'] as const; | ||
|
||
/** 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, | ||
[Opcode.FDIV]: TemporaryDefaultGasCost, | ||
[Opcode.EQ]: TemporaryDefaultGasCost, | ||
[Opcode.LT]: TemporaryDefaultGasCost, | ||
[Opcode.LTE]: TemporaryDefaultGasCost, | ||
[Opcode.AND]: TemporaryDefaultGasCost, | ||
[Opcode.OR]: TemporaryDefaultGasCost, | ||
[Opcode.XOR]: TemporaryDefaultGasCost, | ||
[Opcode.NOT]: TemporaryDefaultGasCost, | ||
[Opcode.SHL]: TemporaryDefaultGasCost, | ||
[Opcode.SHR]: TemporaryDefaultGasCost, | ||
[Opcode.CAST]: TemporaryDefaultGasCost, | ||
// Execution environment | ||
[Opcode.ADDRESS]: TemporaryDefaultGasCost, | ||
[Opcode.STORAGEADDRESS]: TemporaryDefaultGasCost, | ||
[Opcode.ORIGIN]: TemporaryDefaultGasCost, | ||
[Opcode.SENDER]: TemporaryDefaultGasCost, | ||
[Opcode.PORTAL]: TemporaryDefaultGasCost, | ||
[Opcode.FEEPERL1GAS]: TemporaryDefaultGasCost, | ||
[Opcode.FEEPERL2GAS]: TemporaryDefaultGasCost, | ||
[Opcode.FEEPERDAGAS]: TemporaryDefaultGasCost, | ||
[Opcode.CONTRACTCALLDEPTH]: TemporaryDefaultGasCost, | ||
[Opcode.CHAINID]: TemporaryDefaultGasCost, | ||
[Opcode.VERSION]: TemporaryDefaultGasCost, | ||
[Opcode.BLOCKNUMBER]: TemporaryDefaultGasCost, | ||
[Opcode.TIMESTAMP]: TemporaryDefaultGasCost, | ||
[Opcode.COINBASE]: TemporaryDefaultGasCost, | ||
[Opcode.BLOCKL1GASLIMIT]: TemporaryDefaultGasCost, | ||
[Opcode.BLOCKL2GASLIMIT]: TemporaryDefaultGasCost, | ||
[Opcode.BLOCKDAGASLIMIT]: TemporaryDefaultGasCost, | ||
[Opcode.CALLDATACOPY]: TemporaryDefaultGasCost, | ||
// Gas | ||
[Opcode.L1GASLEFT]: TemporaryDefaultGasCost, | ||
[Opcode.L2GASLEFT]: TemporaryDefaultGasCost, | ||
[Opcode.DAGASLEFT]: TemporaryDefaultGasCost, | ||
// Control flow | ||
[Opcode.JUMP]: TemporaryDefaultGasCost, | ||
[Opcode.JUMPI]: TemporaryDefaultGasCost, | ||
[Opcode.INTERNALCALL]: TemporaryDefaultGasCost, | ||
[Opcode.INTERNALRETURN]: TemporaryDefaultGasCost, | ||
// Memory | ||
[Opcode.SET]: TemporaryDefaultGasCost, | ||
[Opcode.MOV]: TemporaryDefaultGasCost, | ||
[Opcode.CMOV]: TemporaryDefaultGasCost, | ||
// World state | ||
[Opcode.SLOAD]: TemporaryDefaultGasCost, | ||
[Opcode.SSTORE]: TemporaryDefaultGasCost, | ||
[Opcode.NOTEHASHEXISTS]: TemporaryDefaultGasCost, | ||
[Opcode.EMITNOTEHASH]: TemporaryDefaultGasCost, | ||
[Opcode.NULLIFIEREXISTS]: TemporaryDefaultGasCost, | ||
[Opcode.EMITNULLIFIER]: TemporaryDefaultGasCost, | ||
[Opcode.L1TOL2MSGEXISTS]: TemporaryDefaultGasCost, | ||
[Opcode.HEADERMEMBER]: TemporaryDefaultGasCost, | ||
[Opcode.EMITUNENCRYPTEDLOG]: TemporaryDefaultGasCost, | ||
[Opcode.SENDL2TOL1MSG]: TemporaryDefaultGasCost, | ||
// External calls | ||
[Opcode.CALL]: TemporaryDefaultGasCost, | ||
[Opcode.STATICCALL]: TemporaryDefaultGasCost, | ||
[Opcode.DELEGATECALL]: TemporaryDefaultGasCost, | ||
[Opcode.RETURN]: TemporaryDefaultGasCost, | ||
[Opcode.REVERT]: TemporaryDefaultGasCost, | ||
// Gadgets | ||
[Opcode.KECCAK]: TemporaryDefaultGasCost, | ||
[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 | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,9 @@ | ||
import { Fr } from '@aztec/circuits.js'; | ||
|
||
import { GasCost, GasDimensions } from './avm_gas_cost.js'; | ||
import { TaggedMemory } from './avm_memory_types.js'; | ||
import { AvmContractCallResults } from './avm_message_call_result.js'; | ||
import { OutOfGasError } from './errors.js'; | ||
|
||
/** | ||
* A few fields of machine state are initialized from AVM session inputs or call instruction arguments | ||
|
@@ -35,7 +37,7 @@ export class AvmMachineState { | |
/** | ||
* Signals that execution should end. | ||
* AvmContext execution continues executing instructions until the machine state signals "halted" | ||
* */ | ||
*/ | ||
public halted: boolean = false; | ||
/** Signals that execution has reverted normally (this does not cover exceptional halts) */ | ||
private reverted: boolean = false; | ||
|
@@ -52,6 +54,28 @@ export class AvmMachineState { | |
return new AvmMachineState(state.l1GasLeft, state.l2GasLeft, state.daGasLeft); | ||
} | ||
|
||
/** | ||
* Consumes the given gas. | ||
* Should any of the gas dimensions get depleted, it sets all gas left to zero and triggers | ||
* an exceptional halt by throwing an OutOfGasError. | ||
*/ | ||
public consumeGas(gasCost: Partial<GasCost>) { | ||
// Assert there is enough gas on every dimension. | ||
const outOfGasDimensions = GasDimensions.filter( | ||
dimension => this[`${dimension}Left`] - (gasCost[dimension] ?? 0) < 0, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the YP says that it has to be strictly bigger than 0? It might be a mistake (I don't see why 0 would be wrong if you don't need to spend anything). Would you mind updating that in the YP? (I don't mind if you do it later). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hah, good catch! |
||
); | ||
// If not, trigger an exceptional halt. | ||
// See https://yp-aztec.netlify.app/docs/public-vm/execution#gas-checks-and-tracking | ||
if (outOfGasDimensions.length > 0) { | ||
this.exceptionalHalt(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Revert didn't set gas to zero, that's why I set up a different function. Still, I wonder whether this is needed at all, since the state seems to be discarded once an exception is thrown. |
||
throw new OutOfGasError(outOfGasDimensions); | ||
} | ||
// Otherwise, charge the corresponding gas | ||
for (const dimension of GasDimensions) { | ||
this[`${dimension}Left`] -= gasCost[dimension] ?? 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we losing any static type checking by using a string here or is TS smart enough? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TS is smart enough! GasDimensions is defined |
||
} | ||
} | ||
|
||
/** | ||
* Most instructions just increment PC before they complete | ||
*/ | ||
|
@@ -80,6 +104,15 @@ export class AvmMachineState { | |
this.output = output; | ||
} | ||
|
||
/** | ||
* Flag an exceptional halt. Clears gas left and sets the reverted flag. No output data. | ||
*/ | ||
protected exceptionalHalt() { | ||
GasDimensions.forEach(dimension => (this[`${dimension}Left`] = 0)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @fcarreiro @spalladino There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Maddiaa0 on this one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this handled by the public VM? i.e. you don't need to prove it to the kernel, the VM says so. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it. Even still, I'm not positive on what the outputs should be. Suppose I specify an l2 gas limit of 10, and i try to run two opcodes, one that consumes 1, and another that would have consumed 100. Do we charge the user 1 gas, or 10? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, I understand this is "just" the simulation side of things, the proof is produced by the circuit. And this clearing of gas is specified in the yellow paper. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I understand we always charge them all of their gas (in all dimensions!) if there is an out of gas. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we would charge them the gas from all dimensions would we? e.g. I wouldn't lose all my L2 gas if I emitted a note hash right at the start of public execution that breached my DA gas limit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree, but setting all gas dimensions to zero in the event of an OOG as specified in the YP seems to point at that. Who should we talk to if we wanted to change that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the YP should be taken as "malleable" in most things gas-related. It doesn't really change much about what the VM actually has to do, so I would defer to protocol experts (i.e., you). However, it might be worth checking if having to calculate the actual gas, etc, has any impact on the actual VM circuit. Maybe resetting all was chosen because of that? I'll bring Sean's attention to this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
this.reverted = true; | ||
this.halted = true; | ||
} | ||
|
||
/** | ||
* Get a summary of execution results for a halted machine state | ||
* @returns summary of execution results | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ import { AvmTestContractArtifact } from '@aztec/noir-contracts.js'; | |
import { jest } from '@jest/globals'; | ||
import { strict as assert } from 'assert'; | ||
|
||
import { AvmMachineState } from './avm_machine_state.js'; | ||
import { TypeTag } from './avm_memory_types.js'; | ||
import { AvmSimulator } from './avm_simulator.js'; | ||
import { | ||
|
@@ -17,26 +18,48 @@ import { | |
initExecutionEnvironment, | ||
initGlobalVariables, | ||
initL1ToL2MessageOracleInput, | ||
initMachineState, | ||
} from './fixtures/index.js'; | ||
import { Add, CalldataCopy, Return } from './opcodes/index.js'; | ||
import { Add, CalldataCopy, Instruction, Return } from './opcodes/index.js'; | ||
import { encodeToBytecode } from './serialization/bytecode_serialization.js'; | ||
|
||
describe('AVM simulator: injected bytecode', () => { | ||
it('Should execute bytecode that performs basic addition', async () => { | ||
const calldata: Fr[] = [new Fr(1), new Fr(2)]; | ||
let calldata: Fr[]; | ||
let ops: Instruction[]; | ||
let bytecode: Buffer; | ||
|
||
// Construct bytecode | ||
const bytecode = encodeToBytecode([ | ||
beforeAll(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you plan to add more tests here? How detailed do we want to be in checking that gas costs are set properly? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm planning on adding more tests for covering the more "dynamic" gas cost functions, for checking reverts, and for testing the different type of gases. |
||
calldata = [new Fr(1), new Fr(2)]; | ||
ops = [ | ||
new CalldataCopy(/*indirect=*/ 0, /*cdOffset=*/ adjustCalldataIndex(0), /*copySize=*/ 2, /*dstOffset=*/ 0), | ||
new Add(/*indirect=*/ 0, TypeTag.FIELD, /*aOffset=*/ 0, /*bOffset=*/ 1, /*dstOffset=*/ 2), | ||
new Return(/*indirect=*/ 0, /*returnOffset=*/ 2, /*copySize=*/ 1), | ||
]); | ||
]; | ||
bytecode = encodeToBytecode(ops); | ||
}); | ||
|
||
it('Should execute bytecode that performs basic addition', async () => { | ||
const context = initContext({ env: initExecutionEnvironment({ calldata }) }); | ||
const { l2GasLeft: initialL2GasLeft } = AvmMachineState.fromState(context.machineState); | ||
const results = await new AvmSimulator(context).executeBytecode(bytecode); | ||
const expectedL2GasUsed = ops.reduce((sum, op) => sum + op.gasCost().l2Gas, 0); | ||
|
||
expect(results.reverted).toBe(false); | ||
expect(results.output).toEqual([new Fr(3)]); | ||
expect(expectedL2GasUsed).toBeGreaterThan(0); | ||
Comment on lines
+45
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd rather have hardcoded numbers for these. Having code compute the expected outputs IMO should be avoided when possible. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't want the tests to break when we updated gas costs for those instructions, but agree on the sentiment of hardcoding. I'll do the change! |
||
expect(context.machineState.l2GasLeft).toEqual(initialL2GasLeft - expectedL2GasUsed); | ||
}); | ||
|
||
it('Should halt if runs out of gas', async () => { | ||
const context = initContext({ | ||
env: initExecutionEnvironment({ calldata }), | ||
machineState: initMachineState({ l2GasLeft: 5 }), | ||
}); | ||
|
||
const results = await new AvmSimulator(context).executeBytecode(bytecode); | ||
expect(results.reverted).toBe(true); | ||
expect(results.output).toEqual([]); | ||
expect(results.revertReason?.name).toEqual('OutOfGasError'); | ||
}); | ||
}); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
import { strict as assert } from 'assert'; | ||
|
||
import type { AvmContext } from '../avm_context.js'; | ||
import { EmptyGasCost, GasCost, GasCosts } from '../avm_gas_cost.js'; | ||
import { BufferCursor } from '../serialization/buffer_cursor.js'; | ||
import { OperandType, deserialize, serialize } from '../serialization/instruction_serialization.js'; | ||
import { Opcode, OperandType, deserialize, serialize } from '../serialization/instruction_serialization.js'; | ||
|
||
type InstructionConstructor = { | ||
new (...args: any[]): Instruction; | ||
|
@@ -14,14 +15,32 @@ type InstructionConstructor = { | |
* It's most important aspects are execute and (de)serialize. | ||
*/ | ||
export abstract class Instruction { | ||
/** | ||
* Consumes gas and executes the instruction. | ||
* This is the main entry point for the instruction. | ||
* @param context - The AvmContext in which the instruction executes. | ||
*/ | ||
public run(context: AvmContext): Promise<void> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm torn about this! But I guess it's ok and it gives us a place to put the PC changes as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Me too. I wanted an "external execute" that did the orchestration and an "internal execute" that had the logic, and let each instruction override either. Problem is I couldn't come up with good names. |
||
context.machineState.consumeGas(this.gasCost()); | ||
return this.execute(context); | ||
} | ||
|
||
/** | ||
* Loads default gas cost for the instruction from the GasCosts table. | ||
* Instruction sub-classes can override this if their gas cost is not fixed. | ||
*/ | ||
public gasCost(): GasCost { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm will this work? E.g., if the gas cost depends on the inputs for the operation, you might not have enough info until you read from memory in I don't know how fancy gas calculation needs to be, so I'll leave it up to you :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there an upper bound we could set for a naive implementation? In a more sophisticated calculation, could it be two parts? First check that I have enough gas to read from memory, then check that I have enough to perform the op? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Another example is calldatacopy where you have mem reads but also a size (known at opcode construction time): there you probably want sth like However, assume that you have a hashing opcode HASH that takes a mem offset for the start of the message to hash, and a mem offset (!) for the size. Then the size is not known at opcode construction time, it will be retrieved from memory (this is to enable hashes in noir over vectors whose size is not fixed at compile time). Then in this case (and probably only in this case, but you should look around) you cannot know how much you'll read.
That separation sounds too add hoc so I'd prefer not. Memory reading is one possible dimension, is there going to be a new one (storage?) and we'll start having 2*dimensions parts? :) If it's going to get too complicated, then maybe it just has to be done in More generally, should gas cost be possible to know without executing anything at all? Then we cannot have indirect memory addresses. Otherwise if some execution (and revertion) is ok then you could just do the gas calculation as you go. (I don't have strong opinions on this beyond not having ad-hoc phases) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the (few) cases where gas cost was intertwined with execution, my plan was to just override There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sounds like probably not then. I think I'd be fine with having the sequencer perform the work to determine the true amount required. Seems like very bounded risk.
👍 |
||
return GasCosts[this.opcode] ?? EmptyGasCost; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we make this fail statically if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, @just-mitch you're right! |
||
} | ||
|
||
/** | ||
* Execute the instruction. | ||
* Instruction sub-classes must implement this. | ||
* As an AvmContext executes its contract code, it calls this function for | ||
* each instruction until the machine state signals "halted". | ||
* @param context - The AvmContext in which the instruction executes. | ||
*/ | ||
public abstract execute(context: AvmContext): Promise<void>; | ||
protected abstract execute(context: AvmContext): Promise<void>; | ||
|
||
/** | ||
* Generate a string representation of the instruction including | ||
|
@@ -61,4 +80,28 @@ export abstract class Instruction { | |
const args = res.slice(1); // Remove opcode. | ||
return new this(...args); | ||
} | ||
|
||
/** | ||
* Returns the stringified type of the instruction. | ||
* Instruction sub-classes should have a static `type` property. | ||
*/ | ||
public get type(): string { | ||
const type = 'type' in this.constructor && (this.constructor.type as string); | ||
if (!type) { | ||
throw new Error(`Instruction class ${this.constructor.name} does not have a static 'type' property defined.`); | ||
} | ||
return type; | ||
} | ||
|
||
/** | ||
* Returns the opcode of the instruction. | ||
* Instruction sub-classes should have a static `opcode` property. | ||
*/ | ||
public get opcode(): Opcode { | ||
const opcode = 'opcode' in this.constructor ? (this.constructor.opcode as Opcode) : undefined; | ||
if (opcode === undefined || Opcode[opcode] === undefined) { | ||
throw new Error(`Instruction class ${this.constructor.name} does not have a static 'opcode' property defined.`); | ||
} | ||
return opcode; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -229,13 +229,15 @@ export class PublicExecutor { | |||||
const hostStorage = new HostStorage(this.stateDb, this.contractsDb, this.commitmentsDb); | ||||||
const worldStateJournal = new AvmPersistableStateManager(hostStorage); | ||||||
const executionEnv = temporaryCreateAvmExecutionEnvironment(execution, globalVariables); | ||||||
const machineState = new AvmMachineState(0, 0, 0); | ||||||
// TODO(@spalladino) Load initial gas from the public execution request | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just as an FYI, there are a few other places with hardcoded/unused gas
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! |
||||||
const machineState = new AvmMachineState(100_000, 100_000, 100_000); | ||||||
|
||||||
const context = new AvmContext(worldStateJournal, executionEnv, machineState); | ||||||
const simulator = new AvmSimulator(context); | ||||||
|
||||||
const result = await simulator.execute(); | ||||||
const newWorldState = context.persistableState.flush(); | ||||||
// TODO(@spalladino) Read gas left from machineState and return it | ||||||
return temporaryConvertAvmResults(execution, newWorldState, result); | ||||||
} | ||||||
|
||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought you wanted to do it per instruction and variable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized most instructions will have a fixed cost, and it's easier to see them in a single place. Instructions with a variable cost can just override
gasCost
. I'll work on one in a future PR to act as an example!