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

+47
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

+161
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

+25
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

+14
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.

lib/ewasm/precompiles/identity.wasm

101 KB
Binary file not shown.

lib/ewasm/precompiles/keccak256.wasm

243 KB
Binary file not shown.

lib/ewasm/precompiles/ripemd160.wasm

255 KB
Binary file not shown.

lib/ewasm/precompiles/sha1.wasm

245 KB
Binary file not shown.

lib/ewasm/precompiles/sha256.wasm

244 KB
Binary file not shown.

lib/ewasm/system/transform-i64.wasm

75 Bytes
Binary file not shown.

lib/ewasm/system/transform-i64.wast

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
;; This module aims to temporarily shim required missing features in wasm-js-api
2+
;; to allow integration of ewasm in js. An example of that is the lack of support
3+
;; for i64 values in the interface between wasm and js.
4+
;; It currently implements only those shims necessary for a few precompiles, but will
5+
;; have to be extended to support the full EEI interface.
6+
(module
7+
(import "interface" "useGas" (func $useGas (param i32 i32)))
8+
9+
(export "useGas" (func $useGasShim))
10+
11+
;; Use gas takes a i64 parameter `amount`. This shim takes the i64 value
12+
;; and breaks it into two 32 bit values `high` and `low` which js can natively
13+
;; support.
14+
(func $useGasShim
15+
(param $amount i64)
16+
(call $useGas
17+
(i32.wrap/i64 (i64.shr_u (get_local $amount) (i64.const 32)))
18+
(i32.wrap/i64 (get_local $amount))
19+
)
20+
)
21+
)

lib/exceptions.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,17 @@ function VmError (error) {
1515
this.errorType = 'VmError'
1616
}
1717

18+
/**
19+
* This exception is thrown when ewasm modules
20+
* call `finish`, to halt execution immediately.
21+
*/
22+
function FinishExecution (message) {
23+
this.message = message
24+
this.name = this.errorType = 'FinishExecution'
25+
}
26+
1827
module.exports = {
1928
ERROR: ERROR,
20-
VmError: VmError
29+
VmError: VmError,
30+
FinishExecution: FinishExecution
2131
}

lib/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ VM.deps = {
4040
* @param {Boolean} opts.activatePrecompiles create entries in the state tree for the precompiled contracts
4141
* @param {Boolean} opts.allowUnlimitedContractSize allows unlimited contract sizes while debugging. By setting this to `true`, the check for contract size limit of 24KB (see [EIP-170](https://git.io/vxZkK)) is bypassed. (default: `false`; ONLY set to `true` during debugging)
4242
* @param {Boolean} opts.emitFreeLogs Changes the behavior of the LOG opcode, the gas cost of the opcode becomes zero and calling it using STATICCALL won't throw. (default: `false`; ONLY set to `true` during debugging)
43+
* @param {Boolean} opts.enableEwasmPrecompiles enable EWASM precompiles. (default: `false`; Experimental feature)
4344
*/
4445
function VM (opts = {}) {
4546
this.opts = opts
@@ -81,6 +82,11 @@ function VM (opts = {}) {
8182
this._precompiled['0000000000000000000000000000000000000007'] = num07
8283
this._precompiled['0000000000000000000000000000000000000008'] = num08
8384

85+
if (opts.enableEwasmPrecompiles) {
86+
const ewasmPrecompiles = require('./ewasm').precompiles
87+
Object.assign(this._precompiled, ewasmPrecompiles)
88+
}
89+
8490
AsyncEventEmitter.call(this)
8591
}
8692

lib/runCall.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const ethUtil = require('ethereumjs-util')
44
const BN = ethUtil.BN
55
const exceptions = require('./exceptions.js')
66
const { StorageReader } = require('./state')
7+
const EwasmContract = require('./ewasm').Contract
8+
const runEwasm = require('./runEwasm')
79

810
const ERROR = exceptions.ERROR
911

@@ -219,7 +221,12 @@ module.exports = function (opts, cb) {
219221
}
220222

221223
// run Code through vm
222-
var codeRunner = isCompiled ? self.runJIT : self.runCode
224+
let codeRunner
225+
if (isCompiled) {
226+
codeRunner = code instanceof EwasmContract ? runEwasm : self.runJIT
227+
} else {
228+
codeRunner = self.runCode
229+
}
223230
codeRunner.call(self, runCodeOpts, parseRunResult)
224231

225232
function parseRunResult (err, results) {

lib/runEwasm.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const Contract = require('./ewasm').Contract
2+
3+
module.exports = (opts, cb) => {
4+
if (!(opts.code instanceof Contract)) {
5+
throw new Error('Invalid ewasm contract')
6+
}
7+
8+
const results = opts.code.run(opts)
9+
results.account = opts.account
10+
cb(results.exceptionError, results)
11+
}

tests/BlockchainTestsRunner.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ module.exports = function runBlockchainTest (options, testData, t, cb) {
3535
var vm = new VM({
3636
state: state,
3737
blockchain: blockchain,
38-
hardfork: options.forkConfig.toLowerCase()
38+
hardfork: options.forkConfig.toLowerCase(),
39+
enableEwasmPrecompiles: options.enableEwasmPrecompiles
3940
})
4041
var genesisBlock = new Block({ hardfork: options.forkConfig.toLowerCase() })
4142

tests/GeneralStateTestsRunner.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ function runTestCase (options, testData, t, cb) {
5757
}
5858
vm = new VM({
5959
state: state,
60-
hardfork: options.forkConfig.toLowerCase()
60+
hardfork: options.forkConfig.toLowerCase(),
61+
enableEwasmPrecompiles: options.enableEwasmPrecompiles
6162
})
6263
testUtil.setupPreConditions(state, testData, done)
6364
},

tests/tester.js

+2
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ function runTests (name, runnerArgs, cb) {
192192
runnerArgs.gasLimit = argv.gas
193193
runnerArgs.value = argv.value
194194

195+
runnerArgs.enableEwasmPrecompiles = argv.ewasm
196+
195197
// runnerArgs.vmtrace = true; // for VMTests
196198

197199
if (argv.customStateTest) {

0 commit comments

Comments
 (0)