Files: 84e730af9e78896d112fb5818ef323f5d767f569 / index.js
10113 bytesRaw
1 | /** |
2 | * This implements the Ethereum Kernel |
3 | * Kernels must implement two methods `codeHandler` and `callHandler` (and `linkHandler` for sharding) |
4 | * The Kernel Contract handles the following |
5 | * - Interprocess communications |
6 | * - Intializing the VM and exposes ROM to it (codeHandler) |
7 | * - Expose namespace which VM instance exists and Intializes the Environment (callHandler) |
8 | * - Provides some built in contract (runTx, runBlock) |
9 | * - Provides resource sharing and limiting via gas |
10 | * |
11 | * All State should be stored in the Environment. |
12 | * |
13 | */ |
14 | |
15 | // The Kernel Exposes this Interface to VM instances it makes |
16 | const Interface = require('./interface.js') |
17 | |
18 | // The Kernel Stores all of its state in the Environment. The Interface is used |
19 | // to by the VM to retrive infromation from the Environment. |
20 | const Environment = require('./environment.js') |
21 | const DebugInterface = require('./debugInterface.js') |
22 | const Address = require('./deps/address.js') |
23 | const U256 = require('./deps/u256.js') |
24 | const Utils = require('./deps/utils.js') |
25 | const Transaction = require('./deps/transaction.js') |
26 | const Precompile = require('./precompile.js') |
27 | |
28 | const identityContract = new Address('0x0000000000000000000000000000000000000004') |
29 | const meteringContract = new Address('0x000000000000000000000000000000000000000A') |
30 | const transcompilerContract = new Address('0x000000000000000000000000000000000000000B') |
31 | |
32 | module.exports = class Kernel { |
33 | // runs some code in the VM |
34 | constructor (environment = new Environment()) { |
35 | this.environment = environment |
36 | this._runningOps = Promise.resolve() |
37 | |
38 | // this.environment.addAccount(identityContract, {}) |
39 | // this.environment.addAccount(meteringContract, {}) |
40 | // this.environment.addAccount(transcompilerContract, {}) |
41 | } |
42 | |
43 | // handles running code. |
44 | // NOTE: it assumes that wasm will raise an exception if something went wrong, |
45 | // otherwise execution succeeded |
46 | codeHandler (code, ethInterface = new Interface(new Environment(), this)) { |
47 | // TODO remove |
48 | ethInterface.kernel = this |
49 | const debugInterface = new DebugInterface(ethInterface.environment) |
50 | const module = WebAssembly.Module(code) |
51 | const imports = { |
52 | 'ethereum': ethInterface.exportTable, |
53 | 'debug': debugInterface.exportTable, |
54 | |
55 | // export this for Rust |
56 | // FIXME: remove once Rust has proper imports, see https://github.com/ethereum/evm2.0-design/issues/15 |
57 | 'spectest': ethInterface.exportTable, |
58 | |
59 | // export this for Binaryen |
60 | // FIXME: remove once C has proper imports, see https://github.com/ethereum/evm2.0-design/issues/16 |
61 | 'env': ethInterface.exportTable |
62 | } |
63 | // add shims |
64 | imports.ethereum.useGas = ethInterface.shims.exports.useGas |
65 | imports.ethereum.getGasLeft = ethInterface.shims.exports.getGasLeft |
66 | imports.ethereum.call = ethInterface.shims.exports.call |
67 | |
68 | const instance = this.instance = WebAssembly.Instance(module, imports) |
69 | |
70 | ethInterface.setModule(instance) |
71 | debugInterface.setModule(instance) |
72 | |
73 | if (instance.exports.main) { |
74 | instance.exports.main() |
75 | } |
76 | return this.onDone() |
77 | } |
78 | |
79 | // returns a promise that resolves when the wasm instance is done running |
80 | async onDone () { |
81 | let prevOps |
82 | while (prevOps !== this._runningOps) { |
83 | prevOps = this._runningOps |
84 | await this._runningOps |
85 | } |
86 | } |
87 | |
88 | // loads code from the merkle trie and delegates the message |
89 | // Detects if code is EVM or WASM |
90 | // Detects if the code injection is needed |
91 | // Detects if transcompilation is needed |
92 | messageHandler (call) { |
93 | // FIXME: this is here until these two contracts are compiled to WASM |
94 | // The two special contracts (precompiles now, but will be real ones later) |
95 | if (call.to.equals(meteringContract)) { |
96 | return Precompile.meteringInjector(call) |
97 | } else if (call.to.equals(transcompilerContract)) { |
98 | return Precompile.transcompiler(call) |
99 | } else if (call.to.equals(identityContract)) { |
100 | return Precompile.identity(call) |
101 | } |
102 | |
103 | let account = this.environment.state.get(call.to.toString()) |
104 | if (!account) { |
105 | throw new Error('Account not found: ' + call.to.toString()) |
106 | } |
107 | |
108 | let code = Uint8Array.from(account.get('code')) |
109 | if (code.length === 0) { |
110 | throw new Error('Contract not found') |
111 | } |
112 | |
113 | if (!Utils.isWASMCode(code)) { |
114 | // throw new Error('Not an eWASM contract') |
115 | |
116 | // Transcompile code |
117 | // FIXME: decide if these are the right values here: from: 0, gasLimit: 0, value: 0 |
118 | code = this.messageHandler({ from: Address.zero(), to: transcompilerContract, gasLimit: 0, value: new U256(0), data: code }).returnValue |
119 | |
120 | if (code[0] === 0) { |
121 | code = code.slice(1) |
122 | } else { |
123 | throw new Error('Transcompilation failed: ' + Buffer.from(code).slice(1).toString()) |
124 | } |
125 | } |
126 | |
127 | // creats a new Kernel |
128 | const environment = new Environment() |
129 | environment.parent = this |
130 | |
131 | // copy the transaction details |
132 | environment.code = code |
133 | environment.address = call.to |
134 | // FIXME: make distinction between origin and caller |
135 | environment.origin = call.from |
136 | environment.caller = call.from |
137 | environment.callData = call.data |
138 | environment.callValue = call.value |
139 | environment.gasLeft = call.gasLimit |
140 | |
141 | environment.callHandler = this.callHandler.bind(this) |
142 | environment.createHandler = this.createHandler.bind(this) |
143 | |
144 | const kernel = new Kernel(environment) |
145 | kernel.codeHandler(code, new Interface(environment)) |
146 | |
147 | // self destructed |
148 | if (environment.selfDestruct) { |
149 | const balance = this.state.get(call.to.toString()).get('balance') |
150 | const beneficiary = this.state.get(environment.selfDestructAddress) |
151 | beneficiary.set('balance', beneficiary.get('balance').add(balance)) |
152 | this.state.delete(call.to.toString()) |
153 | } |
154 | |
155 | // generate new stateroot |
156 | // this.environment.state.set(address, { stateRoot: stateRoot }) |
157 | |
158 | return { |
159 | executionOutcome: 1, // success |
160 | gasLeft: new U256(environment.gasLeft), |
161 | gasRefund: new U256(environment.gasRefund), |
162 | returnValue: environment.returnValue, |
163 | selfDestruct: environment.selfDestruct, |
164 | selfDestructAddress: environment.selfDestructAddress, |
165 | logs: environment.logs |
166 | } |
167 | } |
168 | |
169 | createHandler (create) { |
170 | let code = create.data |
171 | |
172 | // Inject metering |
173 | if (Utils.isWASMCode(code)) { |
174 | // FIXME: decide if these are the right values here: from: 0, gasLimit: 0, value: 0 |
175 | code = this.callHandler({ from: Address.zero(), to: meteringContract, gasLimit: 0, value: new U256(0), data: code }).returnValue |
176 | |
177 | if (code[0] === 0) { |
178 | code = code.slice(1) |
179 | } else { |
180 | throw new Error('Metering injection failed: ' + Buffer.from(code).slice(1).toString()) |
181 | } |
182 | } |
183 | |
184 | let address = Utils.newAccountAddress(create.from, code) |
185 | |
186 | this.environment.addAccount(address.toString(), { |
187 | balance: create.value, |
188 | code: code |
189 | }) |
190 | |
191 | // Run code and take return value as contract code |
192 | // FIXME: decide if these are the right values here: value: 0, data: '' |
193 | code = this.callHandler({ from: create.from, to: address, gasLimit: create.gasLimit, value: new U256(0), data: new Uint8Array() }).returnValue |
194 | |
195 | // FIXME: special handling for selfdestruct |
196 | |
197 | this.environment.state.get(address.toString()).set('code', code) |
198 | |
199 | return { |
200 | executionOutcome: 1, // success |
201 | gasLeft: new U256(this.environment.gasLeft), |
202 | gasRefund: new U256(this.environment.gasRefund), |
203 | accountCreated: address, |
204 | logs: this.environment.logs |
205 | } |
206 | } |
207 | |
208 | // run tx; the tx message handler |
209 | runTx (tx, environment = new Environment()) { |
210 | this.environment = environment |
211 | |
212 | if (Buffer.isBuffer(tx) || typeof tx === 'string') { |
213 | tx = new Transaction(tx) |
214 | if (!tx.valid) { |
215 | throw new Error('Invalid transaction signature') |
216 | } |
217 | } |
218 | |
219 | // look up sender |
220 | let fromAccount = this.environment.state.get(tx.from.toString()) |
221 | if (!fromAccount) { |
222 | throw new Error('Sender account not found: ' + tx.from.toString()) |
223 | } |
224 | |
225 | if (fromAccount.get('nonce').gt(tx.nonce)) { |
226 | throw new Error(`Invalid nonce: ${fromAccount.get('nonce')} > ${tx.nonce}`) |
227 | } |
228 | |
229 | fromAccount.set('nonce', fromAccount.get('nonce').add(new U256(1))) |
230 | |
231 | let isCreation = false |
232 | |
233 | // Special case: contract deployment |
234 | if (tx.to.isZero() && (tx.data.length !== 0)) { |
235 | console.log('This is a contract deployment transaction') |
236 | isCreation = true |
237 | } |
238 | |
239 | // This cost will not be refunded |
240 | let txCost = 21000 + (isCreation ? 32000 : 0) |
241 | tx.data.forEach((item) => { |
242 | if (item === 0) { |
243 | txCost += 4 |
244 | } else { |
245 | txCost += 68 |
246 | } |
247 | }) |
248 | |
249 | if (tx.gasLimit.lt(new U256(txCost))) { |
250 | throw new Error(`Minimum transaction gas limit not met: ${txCost}`) |
251 | } |
252 | |
253 | if (fromAccount.get('balance').lt(tx.gasLimit.mul(tx.gasPrice))) { |
254 | throw new Error(`Insufficient account balance: ${fromAccount.get('balance').toString()} < ${tx.gasLimit.mul(tx.gasPrice).toString()}`) |
255 | } |
256 | |
257 | // deduct gasLimit * gasPrice from sender |
258 | fromAccount.set('balance', fromAccount.get('balance').sub(tx.gasLimit.mul(tx.gasPrice))) |
259 | |
260 | const handler = isCreation ? this.createHandler.bind(this) : this.callHandler.bind(this) |
261 | let ret = handler({ |
262 | to: tx.to, |
263 | from: tx.from, |
264 | gasLimit: tx.gasLimit - txCost, |
265 | value: tx.value, |
266 | data: tx.data |
267 | }) |
268 | |
269 | // refund unused gas |
270 | if (ret.executionOutcome === 1) { |
271 | fromAccount.set('balance', fromAccount.get('balance').add(tx.gasPrice.mul(ret.gasLeft.add(ret.gasRefund)))) |
272 | } |
273 | |
274 | // save new state? |
275 | |
276 | return { |
277 | executionOutcome: ret.executionOutcome, |
278 | accountCreated: isCreation ? ret.accountCreated : undefined, |
279 | returnValue: isCreation ? undefined : ret.returnValue, |
280 | gasLeft: ret.gasLeft, |
281 | logs: ret.logs |
282 | } |
283 | } |
284 | |
285 | // run block; the block message handler |
286 | runBlock (block, environment = new Environment()) { |
287 | // verify block then run each tx |
288 | block.tx.forEach((tx) => { |
289 | this.runTx(tx, environment) |
290 | }) |
291 | } |
292 | |
293 | // run blockchain |
294 | // runBlockchain () {} |
295 | } |
296 |
Built with git-ssb-web