|
| 1 | +# Evmos Custom EIPs |
| 2 | + |
| 3 | +This document explain how **evmOS** allows chain built on top of it to define custom EIPs to modify the behavior of EVM |
| 4 | +opcodes. |
| 5 | + |
| 6 | +## Custom EIPs |
| 7 | + |
| 8 | +Inside an EVM, every state transition or query is executed by evaluating opcodes. Custom EIPs are functions used to |
| 9 | +change the behavior of these opcodes to tailor the EVM functionalities to fit the app-chain requirements. |
| 10 | + |
| 11 | +Custom EIPs should be defined in an `eips` package inside the `./app/eips/` folder of chains using the **evmOS** |
| 12 | +framework. This organization of custom implementations is not a strict requirement, but is the suggested approach to |
| 13 | +have a clean organization of functionalities. In this file, only the custom modifier should be defined. |
| 14 | + |
| 15 | +Inside this package, custom EIP should be defined in a file called `eips.go`. In this file, the EIPs modifier should be |
| 16 | +defined with the signature: |
| 17 | + |
| 18 | +```go |
| 19 | +func(jt *vm.JumpTable) {} |
| 20 | +``` |
| 21 | + |
| 22 | +where `vm` is the package `"github.com/evmos/evmos/v18/x/evm/core/vm"`. |
| 23 | + |
| 24 | +Custom EIPs are used to modify the behavior of opcodes, which are described by the `operation` structure: |
| 25 | + |
| 26 | +```go |
| 27 | +type operation struct { |
| 28 | + // execute is the operation function |
| 29 | + execute executionFunc |
| 30 | + constantGas uint64 |
| 31 | + dynamicGas gasFunc |
| 32 | + // minStack tells how many stack items are required |
| 33 | + minStack int |
| 34 | + // maxStack specifies the max length the stack can have for this operation |
| 35 | + // to not overflow the stack. |
| 36 | + maxStack int |
| 37 | + |
| 38 | + // memorySize returns the memory size required for the operation |
| 39 | + memorySize memorySizeFunc |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +With the **evmOS** framework, it is possible to modify any of the fields defined in the type via the `operation` setter |
| 44 | +methods: |
| 45 | + |
| 46 | +- `SetExecute`: update the execution logic for the opcode. |
| 47 | + |
| 48 | +- `SetConstantGas`: update the value used for the constant gas cost. |
| 49 | + |
| 50 | +- `SetDynamicGas`: update the function used to compute the dynamic gas cost. |
| 51 | + |
| 52 | +- `SetMinStack`: update the minimum number of items in the stack required to execute the `operation`. |
| 53 | + |
| 54 | +- `SetMaxStack`: update the maximum number of items that will be in the stack after executing the `operation`. |
| 55 | + |
| 56 | +- `SetMemorySize`: the memory size required by the `operation`. |
| 57 | + |
| 58 | +An example for an EIP which modifies the constant gas used for the `CREATE` opcode is reported below: |
| 59 | + |
| 60 | +```go |
| 61 | +// Enable a custom EIP-0000 |
| 62 | +func Enable0000(jt *vm.JumpTable) { |
| 63 | + jt[vm.CREATE].SetConstantGas(1) |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +In the same folder should also be defined tests and contracts used to verify the EIPs logic. |
| 68 | + |
| 69 | +## Activate Custom EIPs |
| 70 | + |
| 71 | +The activation of custom EIPs should be done inside the `config.go` file defined in the `./app/` folder. This file has |
| 72 | +the role of the single source for modify the EVM implementation which is defined in the |
| 73 | +[`x/evm/`](https://github.com/evmos/evmos/tree/main/x/evm) folder |
| 74 | +of **evmOS**. |
| 75 | + |
| 76 | +In this file, 3 main components should be defined: |
| 77 | + |
| 78 | +- The custom EIPs, also called activators. |
| 79 | +- The additional default EIPs enabled. |
| 80 | +- The EVM configurator instance. |
| 81 | + |
| 82 | +All these components will be described in the following sections. |
| 83 | + |
| 84 | +### Opcode & EIP Activators |
| 85 | + |
| 86 | +Activators is the name provided by [Go-ethereum](https://geth.ethereum.org/) to the definition of the structure |
| 87 | +grouping all possible non-default EIPs: |
| 88 | + |
| 89 | +```go |
| 90 | +var activators = map[int]func(*JumpTable){ |
| 91 | + 3855: enable3855, |
| 92 | + ... |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +It can be interpreted as a list of available functionalities that can be toggled to change opcodes behavior. The |
| 97 | +structure is a map where the key is the EIP number in the octal representation, and the value is the custom EIP |
| 98 | +function that has to be evaluated. |
| 99 | + |
| 100 | +In **evmOS**, custom activators should be defined in a structure with the same data type, like in the example below: |
| 101 | + |
| 102 | +```go |
| 103 | +// Activate custom EIPs: 0000, 0001, 0002, etc |
| 104 | +evmosActivators = map[int]func(*vm.JumpTable){ |
| 105 | + 0o000: eips.Enable0000, |
| 106 | + 0o001: eips.Enable0001, |
| 107 | + 0o002: eips.Enable0002, |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +It should be noted that the value of each key in the example is the modifier defined in the `eips` package in the |
| 112 | +example provided at the of the [Custom EIPs](#custom-eips) section. |
| 113 | + |
| 114 | +### Default EIPs |
| 115 | + |
| 116 | +Custom EIPs defined in the `activators` map are not enabled by default. This type is only used to define the list of |
| 117 | +custom functionalities that can be activated. To specify which custom EIP activate, we should modify the |
| 118 | +**evmOS** `x/evm` module params. The parameter orchestrating enabled custom EIPs is the `DefaultExtraEIPs` and |
| 119 | +**evmOS** provide an easy and safe way to customize it. |
| 120 | + |
| 121 | +To specify which activator enable in the chain, a new variable containing a slice of keys of the custom activators |
| 122 | +should be defined. An example is reported below: |
| 123 | + |
| 124 | +```go |
| 125 | +evmosEnabledEIPs = []int64{ |
| 126 | + 0o000, |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +In this way, even though the custom activators defined $3$ new EIPs, we are going to activate only the number `0o000` |
| 131 | + |
| 132 | +### EVM Configurator |
| 133 | + |
| 134 | +The EVM configuration is the type used to modify the EVM configuration before starting a node. The type is defined as: |
| 135 | + |
| 136 | +```go |
| 137 | +type EVMConfigurator struct { |
| 138 | + extendedEIPs map[int]func(*vm.JumpTable) |
| 139 | + extendedDefaultExtraEIPs []int64 |
| 140 | + sealed bool |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +Currently, only 2 customizations are possible: |
| 145 | + |
| 146 | +- `WithExtendedEips`: extended the default available EIPs. |
| 147 | + |
| 148 | +- `WithExtendedDefaultExtraEIPs`: extended the default active EIPs. |
| 149 | + |
| 150 | +It is important to notice that the configurator will only allow to append new entries to the default ones defined by |
| 151 | +**evmOS**. The reason behind this choice is to ensure the correct and safe execution of the virtual machine but still |
| 152 | +allowing partners to customize their implementation. |
| 153 | + |
| 154 | +The `EVMConfigurator` type should be constructed using the builder pattern inside the `init()` function of the file so |
| 155 | +that it is run during the creation of the application. |
| 156 | + |
| 157 | +An example of the usage of the configurator is reported below: |
| 158 | + |
| 159 | +```go |
| 160 | +configurator := evmconfig.NewEVMConfigurator(). |
| 161 | + WithExtendedEips(customActivators). |
| 162 | + WithExtendedDefaultExtraEIPs(defaultEnabledEIPs...). |
| 163 | + Configure() |
| 164 | + |
| 165 | +err := configurator.Configure() |
| 166 | +``` |
| 167 | + |
| 168 | +Errors are raised when the configurator tries to append an item with the same name of one of the default one. Since |
| 169 | +this type is used to configure the EVM before starting the node, it is safe, and suggested, to panic: |
| 170 | + |
| 171 | +```go |
| 172 | +if err != nil { |
| 173 | + panic(err) |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +## Custom EIPs Deep Dive |
| 178 | + |
| 179 | +When the chain receives an EVM transaction, it is handled by the `MsgServer` of the `x/evm` within the method |
| 180 | +`EthereumTx`. The method then calls `ApplyTransaction` where the EVM configuration is created: |
| 181 | + |
| 182 | +```go |
| 183 | +cfg, err := k.EVMConfig(ctx, sdk.ConsAddress(ctx.BlockHeader().ProposerAddress), k.eip155ChainID) |
| 184 | +``` |
| 185 | + |
| 186 | +During the creation of this type, a query is made to retrieve the `x/evm` params. After this step, the request is |
| 187 | +passed inside the `ApplyMessageWithConfig` where a new instance of the EVM is created: |
| 188 | + |
| 189 | +```go |
| 190 | +evm := k.NewEVM(ctx, msg, cfg, tracer, stateDB) |
| 191 | +``` |
| 192 | + |
| 193 | +The `NewEVM` method calls the `NewEVMWithHooks` where a new instance of the virtual machine interpreter is created: |
| 194 | + |
| 195 | +```go |
| 196 | +evm.interpreter = NewEVMInterpreter(evm, config) |
| 197 | +``` |
| 198 | + |
| 199 | +The management of activators is handled in this function: |
| 200 | + |
| 201 | +```go |
| 202 | +func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter { |
| 203 | + // If jump table was not initialised we set the default one. |
| 204 | + if cfg.JumpTable == nil { |
| 205 | + cfg.JumpTable = DefaultJumpTable(evm.chainRules) |
| 206 | + for i, eip := range cfg.ExtraEips { |
| 207 | + // Deep-copy jumptable to prevent modification of opcodes in other tables |
| 208 | + copy := CopyJumpTable(cfg.JumpTable) |
| 209 | + if err := EnableEIP(eip, copy); err != nil { |
| 210 | + // Disable it, so caller can check if it's activated or not |
| 211 | + cfg.ExtraEips = append(cfg.ExtraEips[:i], cfg.ExtraEips[i+1:]...) |
| 212 | + log.Error("EIP activation failed", "eip", eip, "error", err) |
| 213 | + } |
| 214 | + cfg.JumpTable = copy |
| 215 | + } |
| 216 | + } |
| 217 | + |
| 218 | + return &EVMInterpreter{ |
| 219 | + evm: evm, |
| 220 | + cfg: cfg, |
| 221 | + } |
| 222 | +} |
| 223 | +``` |
| 224 | + |
| 225 | +As we can see, a new `JumpTable` is created if it is not received from previous evm executions in the same transaction. |
| 226 | +After that, the function iterate over the `ExtraEips` defined in the configuration. Then, it is checked if the EIP is |
| 227 | +associated with an activator. If yes, the activator function is execute, otherwise an error is returned and the EIP is |
| 228 | +removed from the VM configuration. At this point, all the opcodes are ready to be executed. |
| 229 | + |
| 230 | +## How to Use It |
| 231 | + |
| 232 | +In previous sections has been described required structures and files to use the EVM configurator to enable custom |
| 233 | +EIPs. In this the general procedure is taken into considerations. Two different scenarios are described: |
| 234 | + |
| 235 | +- New chain. |
| 236 | + |
| 237 | +- Running chain. |
| 238 | + |
| 239 | +### New Chain |
| 240 | + |
| 241 | +For a new chain starting from block genesis, the procedure described in the sections above is enough. To summarize it: |
| 242 | + |
| 243 | +- Create the eip file with custom activators. |
| 244 | + |
| 245 | +- Create the config file with custom activators, default EIPs, and the configurator. |
| 246 | + |
| 247 | +After starting the chain, the genesis validation will perform all the required checks and the chain will be ready using |
| 248 | +the new custom EIPs. |
| 249 | + |
| 250 | +### Running Chain |
| 251 | + |
| 252 | +The proper approach to include and enable new EIPs, with the current state of the development, is via coordinate chain |
| 253 | +upgrade. During the chain upgrade it is important to define the custom activators since they are not stored in the |
| 254 | +chain. To enable them there are two possibilities: |
| 255 | + |
| 256 | +- Write a migration to add the new enabled EIPsm during the upgrade. |
| 257 | + |
| 258 | +- After the upgrade, create a governance proposal to modify the `x/evm` params. |
0 commit comments