Skip to content

Commit fc9dc9e

Browse files
committed
Add basic support for ewasm precompiles
Signed-off-by: Sina Mahmoodi <itz.s1na@gmail.com>
1 parent 469c1aa commit fc9dc9e

24 files changed

+310
-4
lines changed

lib/ewasm/contract.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const assert = require('assert')
2+
const Env = require('./env')
3+
const Memory = require('./memory')
4+
5+
/**
6+
* Represents an ewasm module. It instantiates the module
7+
* on `run`, and expects `main` and `memory` to be exported.
8+
* A limited subset of EEI is provided to be imported by
9+
* the module.
10+
*/
11+
module.exports = class Contract {
12+
/**
13+
* @param {BufferSource} code - WASM binary code
14+
*/
15+
constructor (code) {
16+
this._module = new WebAssembly.Module(code)
17+
}
18+
19+
/**
20+
* Instantiates the module, providing a subset of EEI as
21+
* imports, and then executes the exported `main` function.
22+
* @param {Object} opts - Environment data required for the call
23+
* @returns result of execution as an Object.
24+
*/
25+
run (opts) {
26+
const env = new Env(opts)
27+
28+
this._instance = new WebAssembly.Instance(this._module, env.imports)
29+
30+
assert(this._instance.exports.main, 'Wasm module has no main function')
31+
assert(this._instance.exports.memory, 'Wasm module has no memory exported')
32+
33+
this._memory = new Memory(this._instance.exports.memory)
34+
env.setMemory(this._memory)
35+
36+
// Run contract. It throws even on successful finish.
37+
try {
38+
this._instance.exports.main()
39+
} catch (e) {
40+
if (e.errorType !== 'VmError' && e.errorType !== 'FinishExecution') {
41+
throw e
42+
}
43+
}
44+
45+
return env._results
46+
}
47+
}

lib/ewasm/env.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
const assert = require('assert')
2+
const fs = require('fs')
3+
const path = require('path')
4+
const BN = require('bn.js')
5+
const { VmError, ERROR, FinishExecution } = require('../exceptions.js')
6+
7+
const transformerRaw = fs.readFileSync(path.join(__dirname, '/system/transform-i64.wasm'))
8+
const transformerModule = WebAssembly.Module(transformerRaw)
9+
10+
module.exports = class Env {
11+
constructor (data) {
12+
this._data = data
13+
this._results = {
14+
gasUsed: new BN(0)
15+
}
16+
17+
// Wasm-js api doesn't yet support i64 param/return values.
18+
// https://github.com/WebAssembly/proposals/issues/7
19+
this.transformer = WebAssembly.Instance(transformerModule, {
20+
'interface': {
21+
'useGas': this._useGas.bind(this)
22+
}
23+
})
24+
}
25+
26+
get imports () {
27+
return {
28+
ethereum: this.ethereum
29+
}
30+
}
31+
32+
get ethereum () {
33+
return {
34+
getCallValue: this.getCallValue.bind(this),
35+
getCallDataSize: this.getCallDataSize.bind(this),
36+
callDataCopy: this.callDataCopy.bind(this),
37+
useGas: this.transformer.exports.useGas,
38+
finish: this.finish.bind(this),
39+
revert: this.revert.bind(this)
40+
}
41+
}
42+
43+
/**
44+
* Gets the deposited value by the instruction/transaction responsible
45+
* for this execution and loads it into memory at the given location.
46+
* @param {Number} offset - The memory offset to load the value into
47+
*/
48+
getCallValue (offset) {
49+
const vBuf = this._data.value
50+
this._memory.write(offset, 16, vBuf)
51+
}
52+
53+
/**
54+
* Returns size of input data in current environment. This pertains to the
55+
* input data passed with the message call instruction or transaction.
56+
*/
57+
getCallDataSize () {
58+
return this._data.data.length
59+
}
60+
61+
/**
62+
* Copies the input data in current environment to memory. This pertains to
63+
* the input data passed with the message call instruction or transaction.
64+
* @param {Number} resultOffset - The memory offset to load data into
65+
* @param {Number} dataOffset - The offset in the input data
66+
* @param {Number} length - The length of data to copy
67+
*/
68+
callDataCopy (resultOffset, dataOffset, length) {
69+
const data = this._data.data.slice(dataOffset, dataOffset + length)
70+
this._memory.write(resultOffset, length, data)
71+
}
72+
73+
_useGas (high, low) {
74+
const amount = fromI64(high, low)
75+
this.useGas(amount)
76+
}
77+
78+
/**
79+
* Subtracts an amount from the gas counter.
80+
* @param {BN} amount - The amount to subtract to the gas counter
81+
* @throws VmError if gas limit is exceeded
82+
*/
83+
useGas (amount) {
84+
amount = new BN(amount)
85+
const gasUsed = this._results.gasUsed.add(amount)
86+
if (this._data.gasLimit.lt(gasUsed)) {
87+
this._results.exception = 0
88+
this._results.exceptionError = ERROR.OUT_OF_GAS
89+
this._results.gasUsed = this._data.gasLimit
90+
this._results.return = Buffer.from([])
91+
throw new VmError(ERROR.OUT_OF_GAS)
92+
}
93+
94+
this._results.gasUsed = gasUsed
95+
}
96+
97+
/**
98+
* Set the returning output data for the execution.
99+
* This will halt the execution immediately.
100+
* @param {Number} offset - The memory offset of the output data
101+
* @param {Number} length - The length of the output data
102+
* @throws FinishExecution
103+
*/
104+
finish (offset, length) {
105+
let ret = Buffer.from([])
106+
if (length) {
107+
ret = Buffer.from(this._memory.read(offset, length))
108+
}
109+
110+
// 1 = success
111+
this._results.exception = 1
112+
this._results.return = ret
113+
114+
throw new FinishExecution('WASM execution finished, should halt')
115+
}
116+
117+
/**
118+
* Set the returning output data for the execution. This will
119+
* halt the execution immediately and set the execution
120+
* result to "reverted".
121+
* @param {Number} offset - The memory offset of the output data
122+
* @param {Number} length - The length of the output data
123+
* @throws VmError
124+
*/
125+
revert (offset, length) {
126+
let ret = Buffer.from([])
127+
if (length) {
128+
ret = Buffer.from(this._memory.read(offset, length))
129+
}
130+
131+
this._results.exception = 0
132+
this._results.exceptionError = ERROR.REVERT
133+
this._results.gasUsed = this._data.gasLimit
134+
this._results.return = ret
135+
136+
throw new VmError(ERROR.REVERT)
137+
}
138+
139+
setMemory (memory) {
140+
this._memory = memory
141+
}
142+
}
143+
144+
// Converts a 64 bit number represented by `high` and `low`, back to a JS numbers
145+
// Adopted from https://github.com/ewasm/ewasm-kernel/blob/master/EVMimports.js
146+
function fromI64 (high, low) {
147+
if (high < 0) {
148+
// convert from a 32-bit two's compliment
149+
high = 0x100000000 - high
150+
}
151+
152+
// High shouldn't have any bits set between 32-21
153+
assert((high & 0xffe00000) === 0, 'Failed to convert wasm i64 to JS numbers')
154+
155+
if (low < 0) {
156+
// convert from a 32-bit two's compliment
157+
low = 0x100000000 - low
158+
}
159+
// JS only bitshift 32bits, so instead of high << 32 we have high * 2 ^ 32
160+
return (high * 4294967296) + low
161+
}

lib/ewasm/index.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const fs = require('fs')
2+
const path = require('path')
3+
const Contract = require('./contract')
4+
5+
const contracts = {}
6+
const wasmFiles = fs.readdirSync(path.join(__dirname, './precompiles'))
7+
for (let f of wasmFiles) {
8+
const name = f.replace('.wasm', '')
9+
const raw = fs.readFileSync(path.join(__dirname, './precompiles', f))
10+
contracts[name] = new Contract(raw)
11+
}
12+
13+
const precompiles = {
14+
'0000000000000000000000000000000000000002': contracts['sha256'],
15+
'0000000000000000000000000000000000000003': contracts['ripemd160'],
16+
'0000000000000000000000000000000000000004': contracts['identity'],
17+
'0000000000000000000000000000000000000006': contracts['ecadd'],
18+
'0000000000000000000000000000000000000007': contracts['ecmul'],
19+
'0000000000000000000000000000000000000008': contracts['ecpairing']
20+
}
21+
22+
module.exports = {
23+
Contract,
24+
precompiles
25+
}

lib/ewasm/memory.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = class Memory {
2+
constructor (raw) {
3+
this._raw = raw
4+
}
5+
6+
write (offset, length, value) {
7+
const m = new Uint8Array(this._raw.buffer, offset, length)
8+
m.set(value)
9+
}
10+
11+
read (offset, length) {
12+
return new Uint8Array(this._raw.buffer, offset, length)
13+
}
14+
}

lib/ewasm/precompiles/blake2.wasm

249 KB
Binary file not shown.
476 KB
Binary file not shown.

lib/ewasm/precompiles/ecadd.wasm

276 KB
Binary file not shown.

lib/ewasm/precompiles/ecmul.wasm

280 KB
Binary file not shown.

lib/ewasm/precompiles/ecpairing.wasm

625 KB
Binary file not shown.

lib/ewasm/precompiles/ed25519.wasm

428 KB
Binary file not shown.

0 commit comments

Comments
 (0)