Files: 410bf961ea8b42dea73d52435bec504893649c03 / interface.js
17332 bytesRaw
1 | /** |
2 | * This is the Ethereum interface that is exposed to the WASM instance which |
3 | * enables to interact with the Ethereum Environment |
4 | */ |
5 | const Address = require('./address.js') |
6 | const U256 = require('./u256.js') |
7 | const fs = require('fs') |
8 | const path = require('path') |
9 | |
10 | const U128_SIZE_BYTES = 16 |
11 | const ADDRESS_SIZE_BYTES = 20 |
12 | const U256_SIZE_BYTES = 32 |
13 | |
14 | // The interface exposed to the WebAessembly Core |
15 | module.exports = class Interface { |
16 | constructor (environment) { |
17 | this.environment = environment |
18 | const shimBin = fs.readFileSync(path.join(__dirname, '/wasm/interface.wasm')) |
19 | const shimMod = WebAssembly.Module(shimBin) |
20 | const shims = WebAssembly.Instance(shimMod, { |
21 | 'interface': { |
22 | 'useGas': this._useGas.bind(this) |
23 | } |
24 | }) |
25 | this.shims = shims |
26 | } |
27 | |
28 | get exportTable () { |
29 | let exportMethods = [ |
30 | // include all the public methods according to the Ethereum Environment Interface (EEI) r1 |
31 | 'getGasLeft', |
32 | 'getAddress', |
33 | 'getBalance', |
34 | 'getTxOrigin', |
35 | 'getCaller', |
36 | 'getCallValue', |
37 | 'getCallDataSize', |
38 | 'callDataCopy', |
39 | 'callDataCopy256', |
40 | 'getCodeSize', |
41 | 'codeCopy', |
42 | 'getExternalCodeSize', |
43 | 'externalCodeCopy', |
44 | 'getTxGasPrice', |
45 | 'getBlockHash', |
46 | 'getBlockCoinbase', |
47 | 'getBlockTimestamp', |
48 | 'getBlockNumber', |
49 | 'getBlockDifficulty', |
50 | 'getBlockGasLimit', |
51 | 'log', |
52 | 'create', |
53 | 'call', |
54 | 'callCode', |
55 | 'callDelegate', |
56 | 'storageStore', |
57 | 'storageLoad', |
58 | 'return', |
59 | 'selfDestruct' |
60 | ] |
61 | let ret = {} |
62 | exportMethods.forEach((method) => { |
63 | ret[method] = this[method].bind(this) |
64 | }) |
65 | return ret |
66 | } |
67 | |
68 | setModule (mod) { |
69 | this.module = mod |
70 | } |
71 | |
72 | /** |
73 | * Subtracts an amount to the gas counter |
74 | * @param {integer} amount the amount to subtract to the gas counter |
75 | */ |
76 | _useGas (high, low) { |
77 | if (high < 0) { |
78 | // convert from a 32-bit two's compliment |
79 | high = 0x100000000 - high |
80 | } |
81 | |
82 | if (low < 0) { |
83 | // convert from a 32-bit two's compliment |
84 | low = 0x100000000 - low |
85 | } |
86 | |
87 | // JS only bitshift 32bits, so instead of high << 32 we have high * 2 ^ 32 |
88 | const amount = (high * 4294967296) + low |
89 | this.takeGas(amount) |
90 | } |
91 | |
92 | /** |
93 | * Returns the current amount of gas |
94 | * @return {integer} |
95 | */ |
96 | getGasLeft () { |
97 | this.takeGas(2) |
98 | |
99 | return this.environment.gasLeft |
100 | } |
101 | |
102 | /** |
103 | * Gets address of currently executing account and loads it into memory at |
104 | * the given offset. |
105 | * @param {integer} offset |
106 | */ |
107 | getAddress (offset) { |
108 | this.takeGas(2) |
109 | |
110 | this.setMemory(offset, ADDRESS_SIZE_BYTES, this.environment.address.toMemory()) |
111 | } |
112 | |
113 | /** |
114 | * Gets balance of the given account and loads it into memory at the given |
115 | * offset. |
116 | * @param {integer} addressOffset the memory offset to laod the address |
117 | * @param {integer} resultOffset |
118 | */ |
119 | getBalance (addressOffset, offset) { |
120 | this.takeGas(20) |
121 | |
122 | const address = Address.fromMemory(this.getMemory(addressOffset, ADDRESS_SIZE_BYTES)) |
123 | // call the parent contract and ask for the balance of one of its child contracts |
124 | const balance = this.environment.getBalance(address) |
125 | this.setMemory(offset, U128_SIZE_BYTES, balance.toMemory(U128_SIZE_BYTES)) |
126 | } |
127 | |
128 | /** |
129 | * Gets the execution's origination address and loads it into memory at the |
130 | * given offset. This is the sender of original transaction; it is never an |
131 | * account with non-empty associated code. |
132 | * @param {integer} offset |
133 | */ |
134 | getTxOrigin (offset) { |
135 | this.takeGas(2) |
136 | |
137 | this.setMemory(offset, ADDRESS_SIZE_BYTES, this.environment.origin.toMemory()) |
138 | } |
139 | |
140 | /** |
141 | * Gets caller address and loads it into memory at the given offset. This is |
142 | * the address of the account that is directly responsible for this execution. |
143 | * @param {integer} offset |
144 | */ |
145 | getCaller (offset) { |
146 | this.takeGas(2) |
147 | |
148 | this.setMemory(offset, ADDRESS_SIZE_BYTES, this.environment.caller.toMemory()) |
149 | } |
150 | |
151 | /** |
152 | * Gets the deposited value by the instruction/transaction responsible for |
153 | * this execution and loads it into memory at the given location. |
154 | * @param {integer} offset |
155 | */ |
156 | getCallValue (offset) { |
157 | this.takeGas(2) |
158 | |
159 | this.setMemory(offset, U128_SIZE_BYTES, this.environment.callValue.toMemory(U128_SIZE_BYTES)) |
160 | } |
161 | |
162 | /** |
163 | * Get size of input data in current environment. This pertains to the input |
164 | * data passed with the message call instruction or transaction. |
165 | * @return {integer} |
166 | */ |
167 | getCallDataSize () { |
168 | this.takeGas(2) |
169 | |
170 | return this.environment.callData.length |
171 | } |
172 | |
173 | /** |
174 | * Copys the input data in current environment to memory. This pertains to |
175 | * the input data passed with the message call instruction or transaction. |
176 | * @param {integer} offset the offset in memory to load into |
177 | * @param {integer} dataOffset the offset in the input data |
178 | * @param {integer} length the length of data to copy |
179 | */ |
180 | callDataCopy (offset, dataOffset, length) { |
181 | this.takeGas(3 + Math.ceil(length / 32) * 3) |
182 | |
183 | if (length) { |
184 | const callData = this.environment.callData.slice(dataOffset, dataOffset + length) |
185 | this.setMemory(offset, length, callData) |
186 | } |
187 | } |
188 | |
189 | /** |
190 | * Copys the input data in current environment to memory. This pertains to |
191 | * the input data passed with the message call instruction or transaction. |
192 | * @param {integer} offset the offset in memory to load into |
193 | * @param {integer} dataOffset the offset in the input data |
194 | */ |
195 | callDataCopy256 (offset, dataOffset) { |
196 | this.takeGas(3) |
197 | const callData = this.environment.callData.slice(dataOffset, dataOffset + 32) |
198 | this.setMemory(offset, U256_SIZE_BYTES, callData) |
199 | } |
200 | |
201 | /** |
202 | * Gets the size of code running in current environment. |
203 | * @return {interger} |
204 | */ |
205 | getCodeSize () { |
206 | this.takeGas(2) |
207 | |
208 | return this.environment.code.length |
209 | } |
210 | |
211 | /** |
212 | * Copys the code running in current environment to memory. |
213 | * @param {integer} offset the memory offset |
214 | * @param {integer} codeOffset the code offset |
215 | * @param {integer} length the length of code to copy |
216 | */ |
217 | codeCopy (resultOffset, codeOffset, length) { |
218 | this.takeGas(3 + Math.ceil(length / 32) * 3) |
219 | |
220 | if (length) { |
221 | const code = this.environment.code.slice(codeOffset, codeOffset + length) |
222 | this.setMemory(resultOffset, length, code) |
223 | } |
224 | } |
225 | |
226 | /** |
227 | * Get size of an account’s code. |
228 | * @param {integer} addressOffset the offset in memory to load the address from |
229 | * @return {integer} |
230 | */ |
231 | getExternalCodeSize (addressOffset) { |
232 | this.takeGas(20) |
233 | |
234 | const address = Address.fromMemory(this.getMemory(addressOffset, ADDRESS_SIZE_BYTES)) |
235 | const code = this.environment.getCode(address) |
236 | return code.length |
237 | } |
238 | |
239 | /** |
240 | * Copys the code of an account to memory. |
241 | * @param {integer} addressOffset the memory offset of the address |
242 | * @param {integer} resultOffset the memory offset |
243 | * @param {integer} codeOffset the code offset |
244 | * @param {integer} length the length of code to copy |
245 | */ |
246 | externalCodeCopy (addressOffset, resultOffset, codeOffset, length) { |
247 | this.takeGas(20 + Math.ceil(length / 32) * 3) |
248 | |
249 | if (length) { |
250 | const address = Address.fromMemory(this.getMemory(addressOffset, ADDRESS_SIZE_BYTES)) |
251 | let code = this.environment.getCode(address) |
252 | code = code.slice(codeOffset, codeOffset + length) |
253 | this.setMemory(resultOffset, length, code) |
254 | } |
255 | } |
256 | |
257 | /** |
258 | * Gets price of gas in current environment. |
259 | * @return {integer} |
260 | */ |
261 | getTxGasPrice () { |
262 | this.takeGas(2) |
263 | |
264 | return this.environment.gasPrice |
265 | } |
266 | |
267 | /** |
268 | * Gets the hash of one of the 256 most recent complete blocks. |
269 | * @param {integer} number which block to load |
270 | * @param {integer} offset the offset to load the hash into |
271 | */ |
272 | getBlockHash (number, offset) { |
273 | this.takeGas(20) |
274 | |
275 | const diff = this.environment.block.number - number |
276 | let hash |
277 | |
278 | if (diff > 256 || diff <= 0) { |
279 | hash = new U256(0) |
280 | } else { |
281 | hash = new U256(this.environment.getBlockHash(number)) |
282 | } |
283 | this.setMemory(offset, U256_SIZE_BYTES, hash.toMemory()) |
284 | } |
285 | |
286 | /** |
287 | * Gets the block’s beneficiary address and loads into memory. |
288 | * @param offset |
289 | */ |
290 | getBlockCoinbase (offset) { |
291 | this.takeGas(2) |
292 | |
293 | this.setMemory(offset, ADDRESS_SIZE_BYTES, this.environment.block.coinbase.toMemory()) |
294 | } |
295 | |
296 | /** |
297 | * Get the block’s timestamp. |
298 | * @return {integer} |
299 | */ |
300 | getBlockTimestamp () { |
301 | this.takeGas(2) |
302 | |
303 | return this.environment.block.timestamp |
304 | } |
305 | |
306 | /** |
307 | * Get the block’s number. |
308 | * @return {integer} |
309 | */ |
310 | getBlockNumber () { |
311 | this.takeGas(2) |
312 | |
313 | return this.environment.block.number |
314 | } |
315 | |
316 | /** |
317 | * Get the block’s difficulty. |
318 | * @return {integer} |
319 | */ |
320 | getBlockDifficulty (offset) { |
321 | this.takeGas(2) |
322 | |
323 | this.setMemory(offset, U256_SIZE_BYTES, this.environment.block.difficulty.toMemory()) |
324 | } |
325 | |
326 | /** |
327 | * Get the block’s gas limit. |
328 | * @return {integer} |
329 | */ |
330 | getBlockGasLimit () { |
331 | this.takeGas(2) |
332 | |
333 | return this.environment.block.gasLimit |
334 | } |
335 | |
336 | /** |
337 | * Creates a new log in the current environment |
338 | * @param {integer} dataOffset the offset in memory to load the memory |
339 | * @param {integer} length the data length |
340 | * @param {integer} number of topics |
341 | */ |
342 | log (dataOffset, length, numberOfTopics, topic1, topic2, topic3, topic4) { |
343 | if (numberOfTopics < 0 || numberOfTopics > 4) { |
344 | throw new Error('Invalid numberOfTopics') |
345 | } |
346 | |
347 | this.takeGas(375 + length * 8 + numberOfTopics * 375) |
348 | |
349 | const data = length ? this.getMemory(dataOffset, length).slice(0) : new Uint8Array([]) |
350 | const topics = [] |
351 | |
352 | if (numberOfTopics > 0) { |
353 | topics.push(U256.fromMemory(this.getMemory(topic1, U256_SIZE_BYTES))) |
354 | } |
355 | |
356 | if (numberOfTopics > 1) { |
357 | topics.push(U256.fromMemory(this.getMemory(topic2, U256_SIZE_BYTES))) |
358 | } |
359 | |
360 | if (numberOfTopics > 2) { |
361 | topics.push(U256.fromMemory(this.getMemory(topic3, U256_SIZE_BYTES))) |
362 | } |
363 | |
364 | if (numberOfTopics > 3) { |
365 | topics.push(U256.fromMemory(this.getMemory(topic4, U256_SIZE_BYTES))) |
366 | } |
367 | |
368 | this.environment.logs.push({ |
369 | data: data, |
370 | topics: topics |
371 | }) |
372 | } |
373 | |
374 | /** |
375 | * Creates a new contract with a given value. |
376 | * @param {integer} valueOffset the offset in memory to the value from |
377 | * @param {integer} dataOffset the offset to load the code for the new contract from |
378 | * @param {integer} length the data length |
379 | * @param (integer} resultOffset the offset to write the new contract address to |
380 | * @return {integer} Return 1 or 0 depending on if the VM trapped on the message or not |
381 | */ |
382 | create (valueOffset, dataOffset, length, resultOffset) { |
383 | this.takeGas(32000 + length * 200) |
384 | |
385 | const value = U256.fromMemory(this.getMemory(valueOffset, U128_SIZE_BYTES)) |
386 | const data = this.getMemory(dataOffset, length).slice(0) |
387 | const [errorCode, address] = this.environment.create(value, data) |
388 | this.setMemory(resultOffset, ADDRESS_SIZE_BYTES, address.toMemory()) |
389 | return errorCode |
390 | } |
391 | |
392 | /** |
393 | * Sends a message with arbiatary data to a given address path |
394 | * @param {integer} addressOffset the offset to load the address path from |
395 | * @param {integer} valueOffset the offset to load the value from |
396 | * @param {integer} dataOffset the offset to load data from |
397 | * @param {integer} dataLength the length of data |
398 | * @param {integer} resultOffset the offset to store the result data at |
399 | * @param {integer} resultLength |
400 | * @param {integer} gas |
401 | * @return {integer} Returns 1 or 0 depending on if the VM trapped on the message or not |
402 | */ |
403 | call (gas, addressOffset, valueOffset, dataOffset, dataLength, resultOffset, resultLength) { |
404 | this.takeGas(40 + gas) |
405 | |
406 | // Load the params from mem |
407 | const address = Address.fromMemory(this.getMemory(addressOffset, ADDRESS_SIZE_BYTES)) |
408 | const value = U256.fromMemory(this.getMemory(valueOffset, U128_SIZE_BYTES)) |
409 | const data = this.getMemory(dataOffset, dataLength).slice(0) |
410 | |
411 | // Special case for calling into empty account |
412 | if (!this.environment.isAccountPresent(address)) { |
413 | this.takeGas(25000) |
414 | } |
415 | |
416 | // Special case for non-zero value |
417 | if (!value.isZero()) { |
418 | this.takeGas(9000) |
419 | gas += 2300 |
420 | } |
421 | |
422 | const [errorCode, result] = this.environment.call(gas, address, value, data) |
423 | this.setMemory(resultOffset, resultLength, result) |
424 | return errorCode |
425 | } |
426 | |
427 | /** |
428 | * Message-call into this account with an alternative account’s code. |
429 | * @param {integer} addressOffset the offset to load the address path from |
430 | * @param {integer} valueOffset the offset to load the value from |
431 | * @param {integer} dataOffset the offset to load data from |
432 | * @param {integer} dataLength the length of data |
433 | * @param {integer} resultOffset the offset to store the result data at |
434 | * @param {integer} resultLength |
435 | * @param {integer} gas |
436 | * @return {integer} Returns 1 or 0 depending on if the VM trapped on the message or not |
437 | */ |
438 | callCode (gas, addressOffset, valueOffset, dataOffset, dataLength, resultOffset, resultLength) { |
439 | // FIXME: count properly |
440 | this.takeGas(40) |
441 | |
442 | // Load the params from mem |
443 | const address = Address.fromMemory(this.getMemory(addressOffset, ADDRESS_SIZE_BYTES)) |
444 | const value = U256.fromMemory(this.getMemory(valueOffset, U128_SIZE_BYTES)) |
445 | const data = this.getMemory(dataOffset, dataLength).slice(0) |
446 | const [errorCode, result] = this.environment.callCode(gas, address, value, data) |
447 | this.setMemory(resultOffset, resultLength, result) |
448 | return errorCode |
449 | } |
450 | |
451 | /** |
452 | * Message-call into this account with an alternative account’s code, but |
453 | * persisting the current values for sender and value. |
454 | * @param {integer} gas |
455 | * @param {integer} addressOffset the offset to load the address path from |
456 | * @param {integer} valueOffset the offset to load the value from |
457 | * @param {integer} dataOffset the offset to load data from |
458 | * @param {integer} dataLength the length of data |
459 | * @param {integer} resultOffset the offset to store the result data at |
460 | * @param {integer} resultLength |
461 | * @return {integer} Returns 1 or 0 depending on if the VM trapped on the message or not |
462 | */ |
463 | callDelegate (gas, addressOffset, dataOffset, dataLength, resultOffset, resultLength) { |
464 | // FIXME: count properly |
465 | this.takeGas(40) |
466 | |
467 | const data = this.getMemory(dataOffset, dataLength).slice(0) |
468 | const address = Address.fromMemory(this.getMemory(addressOffset, ADDRESS_SIZE_BYTES)) |
469 | const [errorCode, result] = this.environment.callDelegate(gas, address, data) |
470 | this.setMemory(resultOffset, resultLength, result) |
471 | return errorCode |
472 | } |
473 | |
474 | /** |
475 | * store a value at a given path in long term storage which are both loaded |
476 | * from Memory |
477 | * @param {interger} pathOffest the memory offset to load the the path from |
478 | * @param {interger} valueOffset the memory offset to load the value from |
479 | */ |
480 | storageStore (pathOffset, valueOffset) { |
481 | const path = new Buffer(this.getMemory(pathOffset, U256_SIZE_BYTES)).toString('hex') |
482 | // copy the value |
483 | const value = this.getMemory(valueOffset, U256_SIZE_BYTES).slice(0) |
484 | const oldValue = this.environment.state.get(path) |
485 | const valIsZero = value.every((i) => i === 0) |
486 | |
487 | this.takeGas(5000) |
488 | |
489 | // write |
490 | if (!valIsZero && !oldValue) { |
491 | this.takeGas(15000) |
492 | } |
493 | |
494 | // delete |
495 | if (valIsZero && oldValue) { |
496 | this.environment.gasRefund += 15000 |
497 | this.environment.state.delete(path) |
498 | } else { |
499 | this.environment.state.set(path, value) |
500 | } |
501 | } |
502 | |
503 | /** |
504 | * reterives a value at a given path in long term storage |
505 | * @param {interger} pathOffest the memory offset to load the the path from |
506 | * @param {interger} resultOffset the memory offset to load the value from |
507 | */ |
508 | storageLoad (pathOffset, resultOffset) { |
509 | this.takeGas(50) |
510 | |
511 | const path = new Buffer(this.getMemory(pathOffset, U256_SIZE_BYTES)).toString('hex') |
512 | const result = this.environment.state.get(path) || new Uint8Array(32) |
513 | this.setMemory(resultOffset, U256_SIZE_BYTES, result) |
514 | } |
515 | |
516 | /** |
517 | * Halt execution returning output data. |
518 | * @param {integer} offset the offset of the output data. |
519 | * @param {integer} length the length of the output data. |
520 | */ |
521 | return (offset, length) { |
522 | if (length) { |
523 | this.environment.returnValue = this.getMemory(offset, length).slice(0) |
524 | } |
525 | } |
526 | |
527 | /** |
528 | * Halt execution and register account for later deletion giving the remaining |
529 | * balance to an address path |
530 | * @param {integer} offset the offset to load the address from |
531 | */ |
532 | selfDestruct (addressOffset) { |
533 | this.environment.selfDestruct = true |
534 | this.environment.selfDestructAddress = Address.fromMemory(this.getMemory(addressOffset, ADDRESS_SIZE_BYTES)) |
535 | this.environment.gasRefund += 24000 |
536 | } |
537 | |
538 | getMemory (offset, length) { |
539 | return new Uint8Array(this.module.exports.memory, offset, length) |
540 | } |
541 | |
542 | setMemory (offset, length, value) { |
543 | const memory = new Uint8Array(this.module.exports.memory, offset, length) |
544 | memory.set(value) |
545 | } |
546 | |
547 | /* |
548 | * Takes gas from the tank. Only needs to check if there's gas left to be taken, |
549 | * because every caller of this method is trusted. |
550 | */ |
551 | takeGas (amount) { |
552 | if (this.environment.gasLeft < amount) { |
553 | throw new Error('Ran out of gas') |
554 | } |
555 | this.environment.gasLeft -= amount |
556 | } |
557 | } |
558 |
Built with git-ssb-web