ewasm-jsvm
Version:
"ewasm virtual machine for javascript"
583 lines (514 loc) • 22.2 kB
JavaScript
const { ethers } = require('ethers');
const { ERROR, BASE_TX_COST } = require('./constants');
const {Logger, logg} = require('./config');
const {
encodeWithSignature,
encode,
decode,
uint8ArrayToHex,
hexToUint8Array,
randomAddress,
extractAddress,
BN2uint8arr,
toBN,
} = require('./utils.js');
const {
cloneContext,
cloneLogs,
} = require('./persistence.js');
const MAX_RECURSION_LIMIT = 10000;
function instance ({
vmname,
vmcore,
initializeImports,
instantiateModule,
decodeOutput,
encodeInput,
entrypoint,
stateProvider,
}) {
// persistence = {accounts, logs, blocks}
// Internal logger
const ilogger = Logger.get(vmname);
let blockCheckpoint;
const getResource = async (address, stateProvider) => {
if (!address) throw new Error('getResource address missing');
let data = vmcore.persistence.accounts.get(address);
// Get account data from provider
if (data.empty && stateProvider) {
ilogger.get('getResource').debug(address);
const runtimeCode = await stateProvider.getCode(address, blockCheckpoint);
const balance = await stateProvider.getBalance(address, blockCheckpoint);
data.runtimeCode = typeof runtimeCode === 'string' ? hexToUint8Array(runtimeCode) : runtimeCode;
data.balance = toBN(balance);
delete data.empty;
vmcore.persistence.accounts.set(data);
}
return data;
}
const storeAccount = ({address, runtimeCode, balance}) => {
vmcore.persistence.accounts.set({
address,
runtimeCode: typeof runtimeCode === 'string' ? hexToUint8Array(runtimeCode) : runtimeCode,
balance,
});
}
const deploy = (bytecode, wabi, address) => async (...args) => {
ilogger.debug('deploy', ...args);
address = address || randomAddress();
const constructori = await initializeWrap(bytecode, wabi, address, false);
try {
await constructori.main(...args);
} catch (e) {
e.runtime = constructori;
throw e;
}
ilogger.debug('deployed', address);
const instance = await runtime(address, wabi);
instance.logs = constructori.logs;
instance.gas = constructori.gas;
return instance;
}
const runtime = async (address, wabi) => {
ilogger.debug('runtime', address);
let account = await getResource(address, stateProvider);
const runtimeCode = account.runtimeCode;
return initializeWrap(runtimeCode, wabi, address, true);
}
const runtimeSim = (runtimeCode, wabi, address) => {
address = address || randomAddress();
storeAccount({address, runtimeCode, balance: 0});
return initializeWrap(runtimeCode, wabi, address, true);
}
const runtimeFromTransaction = async (txHash, provider) => {
provider = provider || stateProvider;
const tx = await provider.getTransaction(txHash);
const {blockNumber, data, value, from, to, gasLimit, gasPrice} = tx;
const runtimeBytecode = await provider.getCode(to, blockNumber);
blockCheckpoint = blockNumber - 1;
const runtime = await runtimeSim(runtimeBytecode, [], to);
runtime.blockNumber = blockNumber;
const transaction = {
data,
value: toBN(value.toHexString()),
from,
to,
gasLimit: toBN(gasLimit.toHexString()).add(toBN(100000)),
gasPrice: toBN(gasPrice.toHexString()),
};
return {runtime, transaction};
}
// TODO if there are other tx touching the contract, executed before this one, they need to be simulated too
const simulateTransaction = async (txHash) => {
const {runtime, transaction} = await runtimeFromTransaction(txHash);
try {
const result = await runtime.mainRaw(transaction);
blockCheckpoint = null;
runtime.result = result;
return runtime;
} catch(e) {
blockCheckpoint = null;
e.runtime = runtime;
throw e;
}
}
async function initializeWrap (bytecode, wabi=[], address, atRuntime = false) {
bytecode = typeof bytecode === 'string' ? hexToUint8Array(bytecode) : bytecode;
ilogger.get('initializeWrap').debug(address);
if (!wabi.find(fabi => fabi.type === 'constructor')) {
wabi.push({ name: 'constructor', type: 'constructor', stateMutability: 'nonpayable', inputs: [], outputs: []});
}
const getfname = (fabi) => !atRuntime ? 'constructor' : (fabi ? fabi.name : 'main');
let wrappedInstance = {
address,
logs: [],
abi: wabi,
bin: bytecode,
}
const appendtxinfo = (obj) => {
Object.assign(wrappedInstance, obj);
return wrappedInstance;
}
const _finishAction = finishAction(ilogger, vmcore.persistence, address, wabi, appendtxinfo);
const _revertAction = revertAction(ilogger, vmcore.persistence, address, appendtxinfo);
const _startExecution = startExecution({
vmcore,
ilogger,
initializeImports,
instantiateModule,
finishAction: _finishAction,
revertAction: _revertAction,
});
const _wrappedMainRaw = await wrappedMainRaw({ilogger, persistence: vmcore.persistence, _startExecution, address, bytecode, getfname});
const _wrappedMain = await wrappedMain(_wrappedMainRaw, getfname, wabi, bytecode);
wrappedInstance.main = await _wrappedMain();
wrappedInstance.mainRaw = await _wrappedMainRaw();
for (let method of wabi) {
if (method.type === 'fallback') {
wrappedInstance[method.name] = await _wrappedMain();
} else if (method.name !== 'constructor') {
const signature = ethers.utils.id(signatureFull(method)).substring(0, 10);
wrappedInstance[method.name] = await _wrappedMain(signature, method);
}
}
return wrappedInstance;
}
const _storeStateChanges = storeStateChanges(ilogger, vmcore.persistence);
const finishAction = (ilogger, persistence, address, wabi, appendtxinfo) => currentPromise => ({result: answ, gas, context = {}, logs} = {}, e) => {
if (!currentPromise) {
console.log('No queued promise found.'); // throw new Error('No queued promise found.');
return;
}
if (currentPromise.resolved) {
console.error('Promise already resolved');
// throw new Error('Promise already resolved');
}
let result;
if (currentPromise.name === 'constructor') {
// ilogger.get('finishAction_constructor').debug(currentPromise.name, answ);
if (!context[address]) context[address] = {};
context[address].runtimeCode = answ;
result = address;
} else {
const abi = wabi.find(abi => abi.name === currentPromise.name);
ilogger.get('finishAction').debug(currentPromise.name, answ);
if (decodeOutput && answ !== null) result = decodeOutput(answ);
else result = answ !== null && typeof answ !== 'undefined' && abi && abi.outputs ? decode(abi.outputs, answ) : answ;
}
appendtxinfo({
logs: currentPromise.opcodelogs,
gas: gas,
txInfo: currentPromise.txInfo,
});
if (!e) {
_storeStateChanges({accounts: context, logs});
currentPromise.resolve(result);
}
else {
currentPromise.reject(e);
}
currentPromise.resolved = true; // passed by reference
// Needed for evm1 to stop execution
return ERROR.STOP;
}
const revertAction = (ilogger, persistence, address, appendtxinfo) => currentPromise => ({result: answ, gas}) => {
if (!currentPromise) {
console.log('No queued promise found.'); // throw new Error('No queued promise found.');
return;
}
if (currentPromise.resolved) {
throw new Error('Promise already resolved');
}
const error = new Error('Revert: ' + uint8ArrayToHex(answ));
ilogger.get('revertAction').debug(currentPromise.name, answ);
appendtxinfo({
logs: currentPromise.opcodelogs,
gas,
txInfo: currentPromise.txInfo,
});
currentPromise.reject(error);
currentPromise.resolved = true;
return ERROR.STOP;
}
const buildCache = (existingCache) => {
// cache is changed in place, by reference
// context[address] = {balance, runtimeCode, storage} // persistence
// data[index] = {gaslimit, to, data, value, result}
const cache = { data: {}, context: existingCache.context || {}, logs: existingCache.logs || [] };
cache.get = index => cache.data[index];
cache.set = (index, obj) => cache.data[index] = obj;
cache.getAndCheck = (index, txobj) => {
const cachedtx = cache.get(index);
const hexdata = lowtx2hex(txobj);
if (!cachedtx || !cachedtx.result) return;
Object.keys(cachedtx).forEach(key => {
if (key !== 'result' && key !== 'storageInitializeZero' && comparify(cachedtx[key]) !== comparify(hexdata[key])) {
throw new Error(`Cache doesn't match data for key ${key}. Cache: ${cachedtx[key]} vs. ${hexdata[key]}`);
}
});
return cachedtx;
}
return cache;
}
const wrappedMainRaw = ({ilogger, persistence, _startExecution, address, bytecode, getfname}) => (fabi) => (txInfo, existingCache = {}) => new Promise(async (resolve, reject) => {
txInfo = {...txInfo}; // TODO immutable
txInfo.to = txInfo.to || address;
const cache = buildCache(existingCache);
// If we have previous cache, we keep it
if (!cache.context[txInfo.from]) {
cache.context[txInfo.from] = await getResource(txInfo.from, stateProvider);
}
if (!cache.context[txInfo.to]) {
cache.context[txInfo.to] = await getResource(txInfo.to, stateProvider);
}
// constructor TODO: check if constructor
if (!cache.context[txInfo.to].runtimeCode) {
cache.context[txInfo.to].runtimeCode = txInfo.data;
cache.context[txInfo.to].storage = {};
}
// needed, otherwise it cycles;
cache.context[txInfo.from].empty = false;
cache.context[txInfo.to].empty = false;
txInfo.gasUsed = toBN(BASE_TX_COST);
txInfo.storageInitializeZero = !stateProvider;
let currentPromise = {
resolve, reject,
name: getfname(fabi),
methodName: entrypoint ? entrypoint(fabi) : 'main',
txInfo,
data: typeof txInfo.data === 'string' ? hexToUint8Array(txInfo.data) : txInfo.data,
cache,
};
currentPromise.data = currentPromise.data || new Uint8Array([]);
currentPromise.txInfo.data = currentPromise.data;
currentPromise.txInfo.to = address;
if (!currentPromise.cache.context[txInfo.to]) {
currentPromise.cache.context[txInfo.to] = await getResource(txInfo.to, stateProvider);
currentPromise.cache.context[txInfo.to].empty = false;
}
currentPromise.opcodelogs = [];
currentPromise.ologger = ologger(log => {
currentPromise.opcodelogs.push(log);
}, address);
currentPromise.ologger.clear = () => currentPromise.opcodelogs = [];
currentPromise.ologger.get = () => currentPromise.opcodelogs;
currentPromise.count = 0;
const __startExecution = () => _startExecution({
currentPromise,
bytecode,
getCache,
txInfo,
internalCallWrap,
internalCallWrapContinue,
asyncResourceWrap,
asyncResourceWrapContinue,
});
ilogger.get('tx').debug('wrappedMainRaw--' + currentPromise.name, txInfo);
const getCache = () => {
// TODO somehow this gets called after finishAction
// need to see where
return currentPromise.cache;
}
const internalCallWrap = (index, dataObj, context, logs) => {
const newtx = {...currentPromise.txInfo, ...lowtx2hex(dataObj)}
newtx.storageInitializeZero = !stateProvider;
currentPromise.parent = true;
currentPromise.interruptTxObj = { index, newtx, context, logs };
ilogger.debug('internalCallWrap');
}
const internalCallWrapContinue = async () => {
const { index, newtx, context, logs } = currentPromise.interruptTxObj;
ilogger.get('internalCallWrapContinue').debug(index);
const wmodule = await runtime(newtx.to, [], stateProvider);
let result = {};
currentPromise.cache.data[index] = newtx;
try {
result.data = await wmodule.mainRaw(newtx, {context, logs});
result.success = 1;
} catch (e) {
console.error(e);
result.success = 0;
}
currentPromise.cache.data[index].result = result;
currentPromise.interruptTxObj = {};
// restart execution from scratch with updated cache
// TODO: get gas left and forward it
currentPromise.ologger.clear();
__startExecution();
}
const asyncResourceWrap = (account, storageKeys) => {
currentPromise.interruptResourceObj = {account, storageKeys};
currentPromise.count += 1;
ilogger.debug('asyncResourceWrap');
}
const asyncResourceWrapContinue = async() => {
const {account: _account, storageKeys} = currentPromise.interruptResourceObj;
let data = _account;
let account = _account;
if (typeof _account === 'string') {
data = await getResource(_account, stateProvider);
}
else {
account = data.address;
}
// Get storage values if needed
if (storageKeys) {
for (let key of storageKeys) {
let value;
if (stateProvider) {
value = await stateProvider.getStorageAt(account, key, blockCheckpoint);
}
else {
value = '0x0000000000000000000000000000000000000000000000000000000000000000';
}
data.storage[key] = hexToUint8Array(value);
}
}
// We must delete this, to avoid requesting the resource over and over again
delete data.empty;
currentPromise.cache.context[account] = data;
ilogger.get('asyncResourceWrapContinue').debug(data.account, data.balance, Object.keys(currentPromise.cache.context));
currentPromise.interruptResourceObj = {};
if (currentPromise.count > MAX_RECURSION_LIMIT) throw new Error('Max recursion limit reached.');
// restart execution from scratch with updated cache
currentPromise.ologger.clear();
__startExecution();
}
__startExecution();
});
const wrappedMain = (_wrappedMainRaw, getfname, wabi, bytecode) => (signature, fabi) => (...input) => {
const fname = getfname(fabi);
const args = input.slice(0, input.length - 1);
const txInfo = input[input.length - 1];
txInfo.origin = txInfo.origin || txInfo.from;
txInfo.value = txInfo.value || '0x00';
let calldata;
if (encodeInput) calldata = encodeInput(args, fabi);
else {
const calldataTypes = (wabi.find(abi => abi.name === fname || (abi.type === fname && fname === 'constructor')) || {}).inputs;
// If there is no function ABI and only one argument, we assume it is the ABI encoded calldata
if (calldataTypes.length !== args.length && args.length === 1) {
calldata = args[0];
if (typeof calldata === 'string') calldata = hexToUint8Array(calldata);
}
else {
calldata = signature ? encodeWithSignature(signature, calldataTypes, args) : encode(calldataTypes, args);
}
}
if (fname === 'constructor') {
// constructor
txInfo.data = new Uint8Array([...bytecode, ...calldata]);
}
else txInfo.data = calldata;
return _wrappedMainRaw(fabi) (txInfo);
}
const startExecution = ({
vmcore,
ilogger,
initializeImports,
instantiateModule,
finishAction,
revertAction,
}) => ({
currentPromise,
bytecode,
getCache,
internalCallWrap,
internalCallWrapContinue,
asyncResourceWrap,
asyncResourceWrapContinue,
}) => {
ilogger.get('tx').debug('startExecution', currentPromise.txInfo);
ilogger.debug('startExecution', Object.keys(currentPromise.cache.context));
let memoryMap;
const _getMemory = () => {
if (!memoryMap) memoryMap = new WebAssembly.Memory({ initial: 2 }); // Size is in pages.
return memoryMap;
}
const getMemory = () => {
if (currentPromise.minstance) return currentPromise.minstance.exports.memory;
return _getMemory();
}
const ologger = currentPromise.ologger;
const importObj = initializeImports(
vmcore,
currentPromise.txInfo,
internalCallWrap,
asyncResourceWrap,
getMemory,
getCache,
finishAction(currentPromise),
revertAction(currentPromise),
ologger,
);
return instantiateModule(bytecode, importObj).then(async wmodule => {
currentPromise.minstance = wmodule.instance;
currentPromise.importObj = importObj; // near memory access
ologger.debug('--', [], [], getCache(), undefined, undefined, 0, toBN(0), toBN(0));
// _NEAR constructor
if (!wmodule.instance.exports[currentPromise.methodName]) {
return finishAction(currentPromise)(bytecode);
}
let result;
try {
result = await wmodule.instance.exports[currentPromise.methodName]();
} catch (e) {
console.log(e.message);
switch(e.message) {
case ERROR.ASYNC_CALL:
// wasm execution stopped, so it can be restarted
// TODO - restart needs to wait until call result
internalCallWrapContinue();
return;
case ERROR.ASYNC_RESOURCE:
asyncResourceWrapContinue();
return;
case ERROR.STOP:
// this is how we stop the wasm module execution
// for return, revert, etc.
return;
default:
// internal errors - throw error after logs are set
finishAction(currentPromise)({result: null}, e);
return;
}
}
// _NEAR doesn't have a finish opcode for functions that do not return, it just returns here
if (!currentPromise.resolved) {
finishAction(currentPromise)(result);
}
});
}
const vmapi = {
deploy,
runtime,
runtimeSim,
getBlock: tag => vmcore.persistence.blocks.get(tag),
getLogs: () => vmcore.persistence.logs,
// dev purposes
getPersistence: () => vmcore.persistence.accounts,
setContext: _storeStateChanges,
simulateTransaction,
runtimeFromTransaction,
}
return vmapi;
}
const signatureFull = fabi => {
return `${fabi.name}(${fabi.inputs.map(inp => inp.type).join(',')})`;
}
function lowtx2hex(dataObj) {
return {
...dataObj,
to: extractAddress(dataObj.to),
value: uint8ArrayToHex(dataObj.value),
from: extractAddress(dataObj.from),
origin: extractAddress(dataObj.origin),
// gasLimit, gasPrice? transform to hex
}
}
const storeStateChanges = (ilogger, persistence) => (context) => {
ilogger.get('context').debug('storeStateChanges', context.accounts);
ilogger.get('logs').debug('storeStateChanges', context.logs);
persistence.accounts.setBulk(context.accounts);
persistence.logs.setBulk(context.logs);
}
const ologger = (callback, address) => logg('opcodes', Logger.LEVELS.DEBUG, (...args) => {
const [name, input, output, cache, stack, changed, position, gasCost, addlGasCost, refundedGas] = args;
const {context, logs, data} = cache;
const clonedContext = cloneContext(context);
const clonedLogs = cloneLogs(logs);
const clonedStack = stack ? stack.map(val => {
// BN or array
return val instanceof Uint8Array ? val : BN2uint8arr(val);
}) : [];
const log = {name, input, output, logs: clonedLogs, context: clonedContext, contractAddress: address, stack: clonedStack, changed, position: position || 0, gasCost, addlGasCost, refundedGas};
callback(log);
if (Logger.getLevel() === 'DEBUG') return log;
return;
});
function comparify(value) {
if (value instanceof Uint8Array) return uint8ArrayToHex(value);
if (value instanceof Object) return JSON.stringify(value);
return value;
}
module.exports = instance;