diff --git a/package.json b/package.json index f1d8fc01..21b26fbf 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "json-schema-to-typescript": "^11.0.1", "node-stdlib-browser": "^1.2.0", "prettier": "^2.3.2", + "quickjs-emscripten": "^0.26.0", "rimraf": "^3.0.2", "smartweave": "0.4.48", "ts-jest": "^28.0.7", diff --git a/tools/quickjs-handle.mjs b/tools/quickjs-handle.mjs new file mode 100644 index 00000000..7b2691e4 --- /dev/null +++ b/tools/quickjs-handle.mjs @@ -0,0 +1,264 @@ +import { DefaultIntrinsics, getQuickJS } from "quickjs-emscripten"; + +const globalsCode = ` + class ProcessError extends Error { + constructor(message) { + super(message); + this.name = "ProcessError"; + } + } + + // add all stuff from https://cookbook_ao.g8way.io/references/ao.html + let ao = { + _version: "0.0.3", + id: "", + _module: "", + authorities: [], + _ref: 0, + outbox: { + Messages: [], + Spawns: [] + }, + env: {} + } + + let currentState = {}; + + BigInt.prototype.toJSON = function () { + return this.toString(); + }; + + // to be called by SDK when evaluating for already cached state - same as WASM handlers + function __initState(newState) { + console.log('__initState', newState); + currentState = newState; + } + + // to be called by SDK after evaluating a message - same as WASM handlers + function __currentState() { + return JSON.stringify(currentState); + } + + function __getOutbox() { + return JSON.stringify(ao.outbox); + } +`; + +// the example smart contract code loaded from Arweave +// note: processCode MUST HAVE 'function handle(state, message, aoGlobals)' +const processCode = ` + function handle(state, message, aoGlobals) { + console.log('handle', message, aoGlobals, state); + + if (!state.hasOwnProperty('counter')) { + state.counter = 0; + } + + if (message.action == 'increment') { + console.log('inside increment', state.counter); + state.counter++; + return; + } + + if (message.action == 'currentValue') { + return { + result: state.counter + } + } + + throw new ProcessError('unknown action'); + } +`.trim(); + +/*const decorateProcessFn = (processCode) => { + return ` + ${processCode} + + function __handleDecorator() { + return function(messageStringified, aoGlobalsStringified) { + console.log('handleDecorator'); + + // TODO: handle BigInt during parse - but how? maybe introduce some custom type "Amount"? + const message = JSON.parse(messageStringified); + const aoGlobals = JSON.parse(aoGlobalsStringified); + console.log('calling original handle'); + + const result = handle(currentState, message, aoGlobals); + + return JSON.stringify(result); + } + } + + __handleDecorator(); + `; +};*/ + +const decorateProcessFnEval = (processCode) => { + return ` + ${processCode} + + function __handleDecorator(message, aoGlobals) { + console.log('handleDecorator'); + const result = handle(currentState, message, aoGlobals); + return JSON.stringify(result); + } + `; +}; + +async function main() { + const QuickJS = await getQuickJS(); + + // 1. creating the QJS runtime with proper memory/cycles limits + const runtime = QuickJS.newRuntime(); + // TODO: memoryLimit, stack size and interrupt cycles should be configurable? + runtime.setMemoryLimit(1024 * 640); + // Limit stack size + runtime.setMaxStackSize(1024 * 320); + // Interrupt computation after 1024 calls to the interrupt handler + let interruptCycles = 0; + runtime.setInterruptHandler(() => ++interruptCycles > 1024); + + // 2. creating the QJS context with proper intrinsics + const vm = runtime.newContext({ + intrinsics: { + ...DefaultIntrinsics, + Date: false, + Proxy: false, + Promise: false, + MapSet: false, + BigFloat: false, + BigInt: true, + BigDecimal: false + } + }); + + // 3. example of registering functions from Host: registering "console.log" API + const logHandle = vm.newFunction("log", (...args) => { + const nativeArgs = args.map(vm.dump); + console.log("QuickJS:", ...nativeArgs); + }); + const consoleHandle = vm.newObject(); + vm.setProp(consoleHandle, "log", logHandle); + vm.setProp(vm.global, "console", consoleHandle); + consoleHandle.dispose(); + logHandle.dispose(); + + // 4. evaluating globals + console.log("evaluating globals"); + const globalsResult = vm.evalCode(globalsCode); + if (globalsResult.error) { + console.log("Globals eval failed:", vm.dump(globalsResult.error)); + globalsResult.error.dispose(); + } else { + globalsResult.value.dispose(); + } + + const initStateResult = vm.evalCode(`__initState(${JSON.stringify({ "counter": 666 })})`); + if (initStateResult.error) { + console.log("initState failed:", vm.dump(initStateResult.error)); + initStateResult.error.dispose(); + } else { + initStateResult.value.dispose(); + } + + // 5. evaluating decorated process function + + // version with function + /*const handleFnResult = vm.evalCode(decorateProcessFn(processCode)); + if (handleFnResult.error) { + console.log("HandleFn eval failed:", vm.dump(handleFnResult.error)); + handleFnResult.error.dispose(); + } else { + // note: this is a handle to the wrapped process function + const handleFn = vm.unwrapResult(handleFnResult); + // actually calling process function + doCall(handleFn, vm, "increment"); + doCall(handleFn, vm, "increment"); + doCall(handleFn, vm, "increment"); + doCall(handleFn, vm, "increment"); + + const currentCounterValue = doCall(handleFn, vm, "currentValue"); + console.log(currentCounterValue); + + handleFn.dispose(); + }*/ + + // version with evalCode + const handleFnResult = vm.evalCode(decorateProcessFnEval(processCode)); + if (handleFnResult.error) { + console.log("HandleFn eval failed:", vm.dump(handleFnResult.error)); + handleFnResult.error.dispose(); + } else { + handleFnResult.value.dispose(); + } + + // actually calling process function + doCallEval(vm, "increment"); + doCallEval(vm, "increment"); + doCallEval(vm, "increment"); + doCallEval(vm, "increment"); + + + const currentCounterValue = doCallEval(vm, "currentValue"); + console.log(currentCounterValue); + + // 6. test error handling + try { + doCallEval(vm, "foobar"); + } catch (e) { + console.error(e); + } + + function doCallEval(vm, processFunction) { + const evalResult = vm.evalCode( + `__handleDecorator( + ${JSON.stringify({ "action": processFunction })}, + ${JSON.stringify({ "owner": "just_ppe", "id": 123 })} + )`); + + if (evalResult.error) { + const error = vm.dump(evalResult.error); + console.log("eval failed", error); + evalResult.error.dispose(); + throw new Error('Eval error', { cause: error }); + } else { + const resultValue = evalResult.value; + const stringValue = vm.getString(resultValue); + const result = stringValue === "undefined" ? + undefined + : JSON.parse(vm.getString(resultValue)); + resultValue.dispose(); + return result; + } + } + + function doCall(handleFn, vm, processFunction) { + const evalResult = vm.callFunction( + handleFn, + vm.undefined, + vm.newString(JSON.stringify({ "action": processFunction })), + vm.newString(JSON.stringify({ "owner": "just_ppe" }))); + + if (evalResult.error) { + const error = vm.dump(evalResult.error); + console.log("eval failed", error); + evalResult.error.dispose(); + throw new Error(error); + } else { + const resultValue = evalResult.value; + const stringValue = vm.getString(resultValue); + const result = stringValue === "undefined" ? + undefined + : JSON.parse(vm.getString(resultValue)); + resultValue.dispose(); + return result; + } + } + + // btw: if the below throws an error, this means some + // of earlier values was not properly disposed + vm.dispose(); + runtime.dispose(); +} + +main().finally(() => console.log("I'm done here.")); diff --git a/tools/quickjs.mjs b/tools/quickjs.mjs new file mode 100644 index 00000000..2647d813 --- /dev/null +++ b/tools/quickjs.mjs @@ -0,0 +1,79 @@ +import { getQuickJS, newQuickJSAsyncWASMModule } from "quickjs-emscripten"; + +// the example smart contract code loaded from Arweave blockchain +const code = ` + function handle(state, action) { + console.log('handle before timeout'); + const timeoutResult = timeout(100); // no 'await' here, because fuck logic + console.log('handle after timeout:', timeoutResult); + + const someShit = {}; + + for (i = 0; i < 100000; i++) { + someShit[""+i] = i*i; + } + + return 1; + } +`.trim(); + +async function main() { + + // 1. creating the QJS context + const QuickJS = await newQuickJSAsyncWASMModule(); + const runtime = QuickJS.newRuntime(); + // "Should be enough for everyone" -- attributed to B. Gates + // runtime.setMemoryLimit(1024 * 640); + // Limit stack size + runtime.setMaxStackSize(1024 * 320); + let interruptCycles = 0 + runtime.setInterruptHandler((runtime) => { interruptCycles++ }); + + const vm = runtime.newContext(); + + // 2. registering APIs + const logHandle = vm.newFunction("log", (...args) => { + const nativeArgs = args.map(vm.dump) + console.log("QuickJS:", ...nativeArgs) + }); + + const consoleHandle = vm.newObject(); + vm.setProp(consoleHandle, "log", logHandle); + vm.setProp(vm.global, "console", consoleHandle); + consoleHandle.dispose(); + logHandle.dispose(); + + const timeoutHandle = vm.newAsyncifiedFunction("timeout", async (msHandle) => { + const ms = vm.getNumber(msHandle); + console.log("omg, that's an async shit!"); + await timeout(1000); + return vm.newString("check your head!"); + }) + timeoutHandle.consume((fn) => vm.setProp(vm.global, "timeout", fn)) + + // 4. calling the "handle" function + const result = await vm.evalCodeAsync(`(() => { + ${code} + return handle(); +})()`); + + if (result.error) { + console.log('Execution failed:', vm.dump(result.error)); + result.error.dispose(); + } else { + const parsedResult = vm.unwrapResult(result).consume(vm.getNumber); + console.log("result", parsedResult); + console.log("Cycles", interruptCycles); + } + + vm.dispose(); + runtime.dispose(); +} + +main().finally(); + +function timeout(delay) { + return new Promise(function (resolve) { + setTimeout(resolve, delay); + }); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 321c9b44..4128f4af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1219,6 +1219,39 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" +"@jitl/quickjs-ffi-types@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.26.0.tgz#b69323d2df200e327e28fd01afbede20b00d3eae" + integrity sha512-49pRXLrdnhglSm5tzBcoa8Q0H5Gscm9IgzQKLeBWYJnDzLoWw4SLZspRTxfxB168uUmwszWy0uBCG6QZYvDTxw== + +"@jitl/quickjs-wasmfile-debug-asyncify@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.26.0.tgz#381503e658ffee91897abc1b5386713ebaed5d36" + integrity sha512-uApydMFA2re/zR2ITUjpua7AD9wJgoPayyldxAh7c0Y0eh+Q1ttJL54HzfEU6Jh4kEver28YpqaEcj9/4d96bw== + dependencies: + "@jitl/quickjs-ffi-types" "0.26.0" + +"@jitl/quickjs-wasmfile-debug-sync@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.26.0.tgz#2ac807e2ed7fa519ac05e5e2357f0bd1257d3ab6" + integrity sha512-M5BeymxjnFunAWHwfgj3uEv0LvWdzjHugOuieygVMoi+fQ020o4rlCcjbttqZZiFgII8HWV4jbBm4vwsbgbCaw== + dependencies: + "@jitl/quickjs-ffi-types" "0.26.0" + +"@jitl/quickjs-wasmfile-release-asyncify@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.26.0.tgz#a1893d1e818455725e106028039f15e48d318615" + integrity sha512-TLgIZAv292dW2I6PoM2yKPR5t+XqKDZS/Sezy8pi8rsZamDscjt9AM8f2QA+YNpyUPU4zTcaUX3Ccp5bj8d20w== + dependencies: + "@jitl/quickjs-ffi-types" "0.26.0" + +"@jitl/quickjs-wasmfile-release-sync@0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.26.0.tgz#5d8c998ec68e3e7581957dd48dc9351e51e8e92e" + integrity sha512-6VLyG89jAcIv/Y868yVaZD1K/uqguH2eHHL5DCXiYHSR6Q614LeRRtAVXygRu8MtOWKZ0O6Uong/KzJ+1hLEHg== + dependencies: + "@jitl/quickjs-ffi-types" "0.26.0" + "@josephg/resolvable@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@josephg/resolvable/-/resolvable-1.0.1.tgz#69bc4db754d79e1a2f17a650d3466e038d94a5eb" @@ -6625,6 +6658,24 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +quickjs-emscripten-core@0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/quickjs-emscripten-core/-/quickjs-emscripten-core-0.26.0.tgz#10724a09cdeca97b68a954d5c168d327a8356a06" + integrity sha512-RFglP182/9sTYirChNUsQOIbH70goOMe4rVr0G4aJykIk0m9ogglUvhXP7iANOeA9CFCKB93ORp6DJ5KJctbKg== + dependencies: + "@jitl/quickjs-ffi-types" "0.26.0" + +quickjs-emscripten@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/quickjs-emscripten/-/quickjs-emscripten-0.26.0.tgz#d540fce2fc2ba027ade9431ab7d9f3c828114e83" + integrity sha512-0xv4U8lyWDzXbXZUxoNIGATlI7+j57LmeYZRsQDym3yLDJDnG3o9AVNorQniGRw+CmeuMSufNLtfwPw9U/r3QA== + dependencies: + "@jitl/quickjs-wasmfile-debug-asyncify" "0.26.0" + "@jitl/quickjs-wasmfile-debug-sync" "0.26.0" + "@jitl/quickjs-wasmfile-release-asyncify" "0.26.0" + "@jitl/quickjs-wasmfile-release-sync" "0.26.0" + quickjs-emscripten-core "0.26.0" + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"