diff --git a/package.json b/package.json index 06ad71f..15607ae 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "graphql-yoga": "^3.6.0", "hamt-sharding": "^3.0.2", "hive-tx": "^4.3.0", + "isolated-vm": "^4.6.0", "it-pushable": "^1.4.2", "k-bucket": "^5.1.0", "key-did-provider-ed25519": "^1.1.0", @@ -49,7 +50,6 @@ "tiny-secp256k1": "^2.2.3", "uuid": "^8.3.2", "varuint-bitcoin": "^1.1.2", - "vm2": "^3.9.19", "winston": "^3.8.2", "xor-distance": "^2.0.0" }, @@ -75,10 +75,10 @@ "build:clear": "rimraf & tsc", "contracts:deploy": "node --experimental-specifier-resolution=node --max-old-space-size=3072 --loader ts-node/esm src/transactions/scriptDeployContract.ts", "contracts:compile": "cd contracts && live-tsc --src . --dest ./ -p ../live-tsc.json", - "ts-node": "node --experimental-specifier-resolution=node --max-old-space-size=3072 --loader ts-node/esm", - "start": "node --experimental-specifier-resolution=node --max-old-space-size=3072 dist/index.js", - "start:benchmark": "nodemon --exec \"node --experimental-specifier-resolution=node --max-old-space-size=3072 --loader ts-node/esm\" src/benchmarkVm.ts", - "dev": "nodemon --exec \"node --experimental-specifier-resolution=node --max-old-space-size=3072 --loader ts-node/esm\" src/index.ts", + "ts-node": "node --experimental-specifier-resolution=node --max-old-space-size=3072 --no-node-snapshot --loader ts-node/esm", + "start": "node --experimental-specifier-resolution=node --max-old-space-size=3072 --no-node-snapshot dist/index.js", + "start:benchmark": "nodemon --exec \"node --experimental-specifier-resolution=node --max-old-space-size=3072 --no-node-snapshot --loader ts-node/esm\" src/benchmarkVm.ts", + "dev": "nodemon --exec \"node --experimental-specifier-resolution=node --max-old-space-size=3072 --no-node-snapshot --loader ts-node/esm\" src/index.ts", "lint:fix": "eslint --fix \"src/**/*.{ts,tsx}\"", "lint": "eslint \"src/**/*.{ts,tsx}\"", "test:report": "sensible-browser ./coverage/lcov-report/index.html", diff --git a/src/services/chainBridge.ts b/src/services/chainBridge.ts index 475d494..d1ba17c 100644 --- a/src/services/chainBridge.ts +++ b/src/services/chainBridge.ts @@ -8,7 +8,6 @@ import { Collection } from 'mongodb' import networks from './networks' import { WitnessService } from './witness' import type PQueue from 'p-queue' -import { VMScript } from 'vm2' import * as vm from 'vm'; import Pushable from 'it-pushable' import { CommitmentStatus, Contract, ContractCommitment } from '../types/contracts' diff --git a/src/services/contractEngine.ts b/src/services/contractEngine.ts index 66f70e6..1d78a19 100644 --- a/src/services/contractEngine.ts +++ b/src/services/contractEngine.ts @@ -1,5 +1,5 @@ import { Collection } from 'mongodb' -import { NodeVM, VM, VMScript } from 'vm2' +import ivm from 'isolated-vm' import { CID } from 'multiformats' import jsonpatch from 'fast-json-patch' import SHA256 from 'crypto-js/sha256' @@ -30,71 +30,142 @@ export class OutputActions { return this.opStack.push(input) } } -class MockDate extends Date { - - constructor(val) { +let codeTemplate = ` +function wrapper () { + RegExp.prototype.constructor = function () { }; + RegExp.prototype.exec = function () { }; + RegExp.prototype.test = function () { }; + Math.random = function () { }; + class MockDate extends Date { + constructor(val) { if(val) { - if(typeof val === 'string') { - if(val.endsWith('Z')) { - super(val) - } else { - super(val + "Z") - } + if(typeof val === 'string') { + if(val.endsWith('Z')) { + super(val) } else { - super(val) + super(val + "Z") } + } else { + super(val) + } } else { - super(0); + super(0); } - // console.log(this) - // this = new Date(0) - } + } - getTimezoneOffset() { + getTimezoneOffset() { return 0; - } + } - toLocaleString() { + toLocaleString() { return this.toUTCString() - } + } - static now() { + static now() { return 0; + } } -} -let codeTemplate = ` -function wrapper () { - RegExp.prototype.constructor = function () { }; - RegExp.prototype.exec = function () { }; - RegExp.prototype.test = function () { }; - Math.random = function () { }; - - let actions = {}; + class OutputActions { + constructor() { + this.opStack = [] + } - ###ACTIONS### + addHiveOp(input) { + return this.opStack.push(input) + } + } - const execute = async function () { - try { - if (api.action && typeof api.action === 'string' && typeof actions[api.action] === 'function') { - if (api.action !== 'init') { - actions.init = null; - } - await actions[api.action](api.payload); - if(api.payload) { - done(api.payload) - } - done(null); - } else { - done('invalid action'); + Date = MockDate; + let utils = { + SHA256: sha256, + bitcoin: { + ValidateSPV: { + validateHeaderChain: btc_validate_spv_header_chain, + validateProof: btc_validate_spv_proof + }, + ser: { + deserializeSPVProof: btc_ser_deserialize_spv_proof + }, + parseTxHex: btc_parse_tx_hex, + reverseBytes: btc_reverse_bytes, + BTCUtils: { + extractPrevBlockLE: btc_utils_extract_prev_block_le, + extractTimestamp: btc_utils_extract_ts, + extractTimestampLE: btc_utils_extract_ts_le, + extractMerkleRootLE: btc_utils_extract_merkleroot_le, + hash256: btc_utils_hash256, + extractOutputAtIndex: btc_utils_extract_output_at_idx, + extractValue: btc_utils_extract_value + }, + SPVUtils: { + deserializeHex: btc_spv_utils_deserialize_hex + } + } + } + let output = { + setChainActions: set_chain_actions + } + api = api.copy(); + api.transferFunds = async (to, amount) => { + return await (await transfer_funds.applySyncPromise(undefined, [to, amount])).copy() + } + api.withdrawFunds = async (amount) => { + return await (await withdraw_funds.applySyncPromise(undefined, [amount])).copy() + } + let state = { + remoteState: async (id) => { + let result = await (await state_remote.applySyncPromise(undefined, [id])).copy() + return { + pull: async (key) => { + return await (await result.pull.applySyncPromise(undefined, [key])).copy() + }, + ls: async (key) => { + return await (await result.ls.applySyncPromise(undefined, [key])).copy() } - } catch (error) { - done(error); } + }, + pull: async (key) => { + return await (await state_pull.applySyncPromise(undefined, [key])).copy() + }, + update: async (key, value) => { + if (typeof value === 'object') + value = make_external_copy(value).copyInto() + await state_update.applySyncPromise(undefined, [key, value]) + }, + ls: async (key) => { + return await (await state_ls.applySyncPromise(undefined, [key])).copy() + }, + del: async (key) => { + await state_del.applySyncPromise(undefined, [key]) } + } + + let actions = {}; + + ###ACTIONS### - execute(); + const execute = async function () { + try { + if (api.action && typeof api.action === 'string' && typeof actions[api.action] === 'function') { + if (api.action !== 'init') { + actions.init = null; + } + await actions[api.action](api.payload); + if(api.payload) { + done(api.payload) + } + done(null); + } else { + done('invalid action'); + } + } catch (error) { + done(error); + } } - wrapper(); + + execute(); +} +wrapper(); ` export class ContractEngine { @@ -367,6 +438,7 @@ export class ContractEngine { } } + /* async executeContract(id, call, args) { const contractInfo = await this.contractDb.findOne({ id, @@ -397,6 +469,7 @@ export class ContractEngine { }) await vm.run(script, 'vm.js') } + */ /** * Executes a list of operations @@ -432,10 +505,8 @@ export class ContractEngine { } else { codeRaw = this.contractCache[id] } - + let code = codeTemplate.replace('###ACTIONS###', options.codeOverride || codeRaw) - - const script = new VMScript(code).compile() let chainActions = null let stateMerkle @@ -456,75 +527,108 @@ export class ContractEngine { startMerkle = state.startMerkle.toString() } - const includedRecord = await this.self.chainBridge.blockHeaders.findOne({ id: op.included_in }) + const isolate = new ivm.Isolate({ memoryLimit: 128 }) // fixed 128MB memory limit for now, maybe should be part of tx fee calculation + const context = await isolate.createContext() + context.global.setSync('global', context.global.derefInto()) + // Expose a new function to the isolate which will create ExternalCopy of the passed argument + context.global.setSync('make_external_copy', function(arg: any) { + return new ivm.ExternalCopy(arg) + }) - const executeOutput = (await new Promise((resolve, reject) => { - const vm = new NodeVM({ - console: 'inherit', - sandbox: { - log: (msg) => { - console.log(msg) - }, - Date: MockDate, - OutputActions, - utils: { - SHA256: (payloadToHash) => { - if (typeof payloadToHash === 'string') { - return SHA256(payloadToHash).toString(enchex); - } - - return SHA256(JSON.stringify(payloadToHash)).toString(enchex); - }, - bitcoin: { - ValidateSPV, - ser: ser, - parseTxHex: parseTxHex, - reverseBytes: reverse, - BTCUtils, - SPVUtils: utils - } - }, - output: { - setChainActions: (actions) => { - chainActions = (actions.opStack as Array).map(e => ({tx: e})) - } - }, - api: { - action: opData.action, - payload: opData.payload, - input: { - sender: { - type: "DID", - id: op.account_auth - }, - tx_id: op.id, - included_in: op.included_in, - included_block: includedRecord.hive_ref_block, - included_date: includedRecord.hive_ref_date - }, - transferFunds: this.transferFunds, - withdrawFunds: this.withdrawFunds, - getBalance: (accountId: string) => { - return this.self.chainBridge.calculateBalanceSum(accountId, { - // pla: TODO, NEED TO SUPPLY CURRENT BLOCK INFORMATION IN ORDER TO CALC THE BALANCE - } as BlockRef, id) - } - }, - done: () => { - return resolve(state.finish()) - }, - // console: "redirect", - state: state.client, + // log + context.global.setSync('log', (...args) => { + console.log(...args) + }) + + // sha256 + context.global.setSync('sha256', (payloadToHash: string | object) => { + if (typeof payloadToHash === 'string') { + return SHA256(payloadToHash).toString(enchex); + } + + return SHA256(JSON.stringify(payloadToHash)).toString(enchex); + }) + + // btc functions + context.global.setSync('btc_validate_spv_header_chain', ValidateSPV.validateHeaderChain) + context.global.setSync('btc_validate_spv_proof', ValidateSPV.validateProof) + context.global.setSync('btc_ser_deserialize_spv_proof', ser.deserializeSPVProof) + context.global.setSync('btc_parse_tx_hex', parseTxHex) + context.global.setSync('btc_reverse_bytes', reverse) + context.global.setSync('btc_utils_extract_prev_block_le', BTCUtils.extractPrevBlockLE) + context.global.setSync('btc_utils_extract_ts', BTCUtils.extractTimestamp) + context.global.setSync('btc_utils_extract_ts_le', BTCUtils.extractTimestampLE) + context.global.setSync('btc_utils_extract_merkleroot_le', BTCUtils.extractMerkleRootLE) + context.global.setSync('btc_utils_hash256', BTCUtils.hash256) + context.global.setSync('btc_utils_extract_output_at_idx', BTCUtils.extractOutputAtIndex) + context.global.setSync('btc_utils_extract_value', BTCUtils.extractValue) + context.global.setSync('btc_spv_utils_deserialize_hex', utils.deserializeHex) + + // apis + context.global.setSync('set_chain_actions', (actions: OutputActions) => { + chainActions = (actions.opStack as Array).map(e => ({tx: e})) + }) + context.global.setSync('api', new ivm.ExternalCopy({ + action: opData.action, + payload: opData.payload, + input: { + sender: { + type: "DID", + id: op.account_auth }, + tx_id: op.id, + included_in: op.included_in, + included_block: includedRecord.hive_ref_block, + included_date: includedRecord.hive_ref_date + } + })) + context.global.setSync('transfer_funds', new ivm.Reference(async (to: DID, amount: number) => { + let result = await this.transferFunds(to, amount) + return new ivm.ExternalCopy(result) + })) + context.global.setSync('withdraw_funds', new ivm.Reference(async (amount: number) => { + let result = await this.withdrawFunds(amount) + return new ivm.ExternalCopy(result) + })) + context.global.setSync('get_balance', new ivm.Reference(async (accountId: string) => { + let result = await this.self.chainBridge.calculateBalanceSum(accountId, { + // pla: TODO, NEED TO SUPPLY CURRENT BLOCK INFORMATION IN ORDER TO CALC THE BALANCE + } as BlockRef, id) + return new ivm.ExternalCopy(result) + })) + + // state + context.global.setSync('state_remote', new ivm.Reference(async (id: string) => { + let result = await state.client.remoteState(id) + return new ivm.ExternalCopy({ + pull: new ivm.Reference(async (key: string) => new ivm.ExternalCopy(await result.pull(key))), + ls: new ivm.Reference(async (key: string) => new ivm.ExternalCopy(await result.ls(key))), }) - vm.run(script, 'vm.js') - })) as { stateMerkle: string } - stateMerkle = executeOutput.stateMerkle + })) + context.global.setSync('state_pull', new ivm.Reference(async (key: string) => { + let result = await state.client.pull(key) + return new ivm.ExternalCopy(result) + })) + context.global.setSync('state_update', new ivm.Reference(async (key: string, value: any) => { + await state.client.update(key, value) + })) + context.global.setSync('state_ls', new ivm.Reference(async (key: string) => { + let result = await state.client.ls(key) + return new ivm.ExternalCopy(result) + })) + context.global.setSync('state_del', new ivm.Reference(async (key: string) => { + await state.client.del(key) + })) + context.global.setSync('done', () => { + stateMerkle = state.finish().stateMerkle + }) + const compiled = await isolate.compileScript(code) + await compiled.run(context) } this.self.logger.info('new state merkle of executed contract', stateMerkle) diff --git a/src/services/transactionPool.ts b/src/services/transactionPool.ts index 9bcc369..7b3898e 100644 --- a/src/services/transactionPool.ts +++ b/src/services/transactionPool.ts @@ -8,7 +8,6 @@ import { sha256 as hasher } from 'multiformats/hashes/sha2' import BloomFilters from 'bloom-filters' import { CoreService } from '.' import { BlockHeader, TransactionContainer, TransactionDbRecord, TransactionDbStatus, TransactionDbType, TransactionRaw } from '../types' -import { VM, NodeVM, VMScript } from 'vm2' import fs from 'fs/promises' import { isNamedType } from 'graphql/type/definition.js' import * as vm from 'vm';