diff --git a/packages/mocknet/src/server.ts b/packages/mocknet/src/server.ts index a13c3d28..e3f7c3b3 100644 --- a/packages/mocknet/src/server.ts +++ b/packages/mocknet/src/server.ts @@ -113,7 +113,7 @@ export async function buildApp (argv: ParsedArgs = { _: [] }): Promise { app.get('/output-by-origin/:origin', (req, res) => { const origin = req.params.origin - const jigState = storage.getJigStateByOrigin( + const jigState = storage.outputByOrigin( Pointer.fromString(origin) ).orElse(() => { throw new HttpNotFound(`${origin} not found`, { origin }) }) res.status(200).send(serializeOutput(jigState)) @@ -138,7 +138,7 @@ export async function buildApp (argv: ParsedArgs = { _: [] }): Promise { const coinPkg = storage.getPkg('0000000000000000000000000000000000000000000000000000000000000000').get() const bcs = new BCS(coinPkg.abi) const { address, amount } = req.body - const coinLocation = storage.tipFor(coinOrigin) + const coinLocation = storage.tipFor(coinOrigin).map(base16.decode).get() const tx = new Tx() tx.push(new LoadInstruction(coinLocation)) tx.push(new CallInstruction(0, 0, bcs.encode('Coin_send', [amount]))) diff --git a/packages/vm/README.md b/packages/vm/README.md index 88bf4090..b57bf906 100644 --- a/packages/vm/README.md +++ b/packages/vm/README.md @@ -5,12 +5,29 @@ > The Aldea Computer virtual machine implemented in JavaScript. -The VM is really where all the magic happens. The JavaScript implementation currently serves as our reference implementation. +The VM is really where all the magic happens. The JavaScript implementation currently serves as our reference +implementation. -Developers won't usually have need to interract with the VM directly, but instead the VM is depended on by other packages such as the [mocknet](https://github.com/aldeacomputer/aldea-js/tree/main/packages/mocknet) or node implementations. +Developers won't usually have need to interract with the VM directly, but instead the VM is depended on by other +packages such as the [mocknet](https://github.com/aldeacomputer/aldea-js/tree/main/packages/mocknet) or node +implementations. + +## Build + +```bash +yarn i +yarn build +``` + +## Run tests + +```bash +yarn test +``` ## License -Aldea is open source and released under the [Apache-2 License](https://github.com/aldeacomputer/aldea-js/blob/main/packages/vm/LICENSE). +Aldea is open source and released under +the [Apache-2 License](https://github.com/aldeacomputer/aldea-js/blob/main/packages/vm/LICENSE). © Copyright 2023 Run Computer Company, inc. diff --git a/packages/vm/src/args-translator.ts b/packages/vm/src/arguments-pre-processor.ts similarity index 64% rename from packages/vm/src/args-translator.ts rename to packages/vm/src/arguments-pre-processor.ts index 14738385..e9d4b32d 100644 --- a/packages/vm/src/args-translator.ts +++ b/packages/vm/src/arguments-pre-processor.ts @@ -5,7 +5,11 @@ import {AbiAccess} from "./memory/abi-helpers/abi-access.js"; import {AbiArg} from "./memory/abi-helpers/abi-arg.js"; -export class ArgsTranslator { +/** + * Aldea transactions include arguments with indexes pointing to previous parts of the transaction. + * This class is in charge or solving and de referencing thos indexes. + */ +export class ArgumentsPreProcessor { exec: TxExecution private abi: AbiAccess; @@ -14,7 +18,14 @@ export class ArgsTranslator { this.abi = abi } - fix(encoded: Uint8Array, args: AbiArg[]): Uint8Array { + /** + * Solves the references in the encoded data by replacing them with their corresponding values. + * + * @param {Uint8Array} encoded - The encoded data with references. + * @param {AbiArg[]} args - The array of arguments with their types. + * @returns {Uint8Array} - The updated encoded data with solved references. + */ + solveReferences (encoded: Uint8Array, args: AbiArg[]): Uint8Array { const reader = new BufReader(encoded) const indexes = reader.readSeq(r => r.readU8()) const into = new BufWriter() @@ -26,14 +37,14 @@ export class ArgsTranslator { const value = this.exec.stmtAt(idx).asValue() into.writeFixedBytes(value.lift()) } else { - this.translateChunk(reader, ty, into) + this.derefChunk(reader, ty, into) } }) return into.data } - translateChunk(from: BufReader, ty: AbiType, into: BufWriter): void { + private derefChunk (from: BufReader, ty: AbiType, into: BufWriter): void { if (ty.nullable) { return this.translateNullable(from, ty, into) } @@ -80,19 +91,19 @@ export class ArgsTranslator { into.writeBytes(from.readBytes()) break case 'Map': - this.translateMap(from, ty, into) + this.derefMap(from, ty, into) break default: - this.translateComplexType(from, ty, into) + this.derefComplexType(from, ty, into) break } } - private translateNullable(from: BufReader, ty: AbiType, into: BufWriter) { + private translateNullable (from: BufReader, ty: AbiType, into: BufWriter) { const flag = from.readU8() if (flag !== 0) { into.writeU8(1) - this.translateChunk(from, ty.toPresent(), into) + this.derefChunk(from, ty.toPresent(), into) } else { into.writeU8(0) } @@ -102,25 +113,33 @@ export class ArgsTranslator { const length = from.readULEB() into.writeULEB(length) for (let i = 0; i < length; i++) { - this.translateChunk(from, ty.args[0], into) + this.derefChunk(from, ty.args[0], into) } } - private translateMap (from: BufReader, ty: AbiType, into: BufWriter) { + private derefMap (from: BufReader, ty: AbiType, into: BufWriter) { const length = from.readULEB() into.writeULEB(length) for (let i = 0; i < length; i++) { - this.translateChunk(from, ty.args[0], into) - this.translateChunk(from, ty.args[1], into) + this.derefChunk(from, ty.args[0], into) + this.derefChunk(from, ty.args[1], into) } } - private translateComplexType(from: BufReader, ty: AbiType, into: BufWriter) { + /** + * At this point the argument might be a plain object or a Jig. Jigs are always sent + * as references, so we need to dereference them. Objects are traversed by its properties. + * @param from + * @param ty + * @param into + * @private + */ + private derefComplexType (from: BufReader, ty: AbiType, into: BufWriter) { const objDef = this.abi.objectDef(ty.name) if (objDef.isPresent()) { for (const fieldTy of objDef.get().fields) { - this.translateChunk(from, fieldTy.type, into) + this.derefChunk(from, fieldTy.type, into) } return } else { @@ -129,6 +148,5 @@ export class ArgsTranslator { const lifted = stmt.lift() into.writeFixedBytes(lifted) } - } } diff --git a/packages/vm/src/calculate-package-hash.ts b/packages/vm/src/calculate-package-hash.ts new file mode 100644 index 00000000..8e8b0b5d --- /dev/null +++ b/packages/vm/src/calculate-package-hash.ts @@ -0,0 +1,13 @@ +import {BCS, blake3} from "@aldea/core"; + +/** + * Helper function to calculate the id of a package. + * + * @param {string[]} entryPoints - The array of entry point strings. + * @param {Map} sources - The map of source files. + * @return {Uint8Array} The calculated package hash as a Uint8Array. + */ +export function calculatePackageHash (entryPoints: string[], sources: Map): Uint8Array { + const data = BCS.pkg.encode([entryPoints.sort(), sources]) + return blake3.hash(data) +} diff --git a/packages/vm/src/calculate-package-id.ts b/packages/vm/src/calculate-package-id.ts deleted file mode 100644 index 59765e42..00000000 --- a/packages/vm/src/calculate-package-id.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {BCS, blake3} from "@aldea/core"; - -export function calculatePackageId (entryPoints: string[], sources: Map): Uint8Array { - const data = BCS.pkg.encode([entryPoints.sort(), sources]) - return blake3.hash(data) -} diff --git a/packages/vm/src/errors.ts b/packages/vm/src/errors.ts index adf23866..26d244ba 100644 --- a/packages/vm/src/errors.ts +++ b/packages/vm/src/errors.ts @@ -1,5 +1,17 @@ +/** + * Runtime error related to lack of permissions. + */ export class PermissionError extends Error {} + +/** + * Runtime error. It might a throw inside the code, a division by zero or + * any kind of breaking in the Aldea protocol. + */ export class ExecutionError extends Error {} -export class IvariantBroken extends Error {} +/** + * Error raised when a rule that should be followed is broken. For example, + * a jig referencing a jig that does not exist. + */ +export class InvariantBroken extends Error {} diff --git a/packages/vm/src/tx-context/ex-tx-exec-context.ts b/packages/vm/src/exec-context/ex-tx-exec-context.ts similarity index 53% rename from packages/vm/src/tx-context/ex-tx-exec-context.ts rename to packages/vm/src/exec-context/ex-tx-exec-context.ts index fc670613..ab7508e0 100644 --- a/packages/vm/src/tx-context/ex-tx-exec-context.ts +++ b/packages/vm/src/exec-context/ex-tx-exec-context.ts @@ -29,7 +29,74 @@ // } // } // -// export class ExTxExecContext implements ExecContext { +import {ExecContext} from "./exec-context.js"; +import {abiFromBin, base16, Output, Pointer, PubKey} from "@aldea/core"; +import {WasmContainer} from "../wasm-container.js"; +import {ExtendedTx} from "./extended-tx.js"; +import {CompileFn} from "../vm.js"; +import {ExecutionError} from "../errors.js"; +import {calculatePackageHash} from "../calculate-package-hash.js"; +import {Buffer} from "buffer"; +import {Option} from "../support/option.js"; +import {PkgData} from "../storage/pkg-data.js"; + +export class ExTxExecContext implements ExecContext { + exTx: ExtendedTx + private compileFn: CompileFn; + private containers: WasmContainer[]; + + constructor (exTx: ExtendedTx, containers: WasmContainer[], compileFn: CompileFn) { + this.exTx = exTx + this.containers = containers + this.compileFn = compileFn + } + + async compile (entries: string[], sources: Map): Promise { + const id = calculatePackageHash(entries, sources) + + const result = await this.compileFn(entries, sources, new Map()) + + return new PkgData( + abiFromBin(result.output.abi), + Buffer.from(result.output.docs || ''), + entries, + id, + new WebAssembly.Module(result.output.wasm), + sources, + result.output.wasm + ) + } + + inputByOrigin (origin: Pointer): Output { + const output = this.exTx.inputs.find(i => i.origin.equals(origin)); + if (!output) { + throw new ExecutionError(`input not provided. Origin:${origin.toString()}`) + } + return output + } + + outputById (hash: Uint8Array): Output { + let id = base16.encode(hash) + const output = this.exTx.inputs.find(i => i.id === id); + if (!output) { + throw new ExecutionError(`input not provided. id: ${id}`) + } + return output + } + + signers (): PubKey[] { + return this.exTx.tx.signers(); + } + + txHash (): Uint8Array { + return this.exTx.tx.hash; + } + + wasmFromPkgId (pkgId: string): WasmContainer { + return Option.fromNullable(this.containers.find(c => c.id === pkgId)).expect( + new ExecutionError(`Missing package: ${pkgId}`) + ) + } // private exTx: ExtendedTx // private clock: Clock; // private pkgs: PkgRepository @@ -83,5 +150,4 @@ // wasmFromPkgId(pkgId: Uint8Array): WasmContainer { // return this.pkgs.wasmForPackageId(pkgId); // } -// } -// +} diff --git a/packages/vm/src/exec-context/exec-context.ts b/packages/vm/src/exec-context/exec-context.ts new file mode 100644 index 00000000..c9fa6c41 --- /dev/null +++ b/packages/vm/src/exec-context/exec-context.ts @@ -0,0 +1,63 @@ +import {Output, Pointer, PubKey} from "@aldea/core"; +import {WasmContainer} from "../wasm-container.js"; + +import {PkgData} from "../storage/pkg-data.js"; + +/** + * This interface represents everything that is needed for a VM + * to execute a specific transaction. + * + * Notice that all methods are sync. Transaction execution is always sync except for deploys, but + * the VM ensures to simulate sync there too. + */ +export interface ExecContext { + /** + * Retrieves the transaction hash (id) for the transaction being currently executed. + * + * @return {Uint8Array} The transaction hash (32 bytes). + */ + txHash (): Uint8Array + + /** + * Retrieves an output from the context, searching by id. + * + * @param {Uint8Array} id - The ID of the output. + * @return {Output} - The Output object representing the state associated with the given output ID. + */ + outputById (id: Uint8Array): Output + + /** + * Retrieves an output from context matching given origin. + * + * @param {Pointer} origin - The origin to retrieve the output for. + * @return {Output} - The output corresponding to the provided origin. + */ + inputByOrigin (origin: Pointer): Output + + /** + * Retrieves a WasmContainer initialized with the package matching the given id. + * + * @param {string} pkgId - The package ID of the Wasm container to retrieve. + * @return {WasmContainer} - The Wasm container associated with the package ID. + */ + wasmFromPkgId (pkgId: string): WasmContainer + + /** + * Compiles the given entries using the provided sources. + * + * @param {string[]} entries - An array of entry strings to compile. + * @param {Map} sources - A map containing the source files to compile the entries from. + * @return {Promise} A Promise that resolves to the compiled package data. + */ + compile (entries: string[], sources: Map): Promise + + /** + * Retrieves the list of pubkeys that have signed the current tx. + * This allows the VM to exec instruction by instruction and separates + * the execution time from the signature validation time. + * + * @return {PubKey[]} The list of signers represented by an array of PubKey objects. + */ + signers (): PubKey[] +} + diff --git a/packages/vm/src/tx-context/extended-tx.ts b/packages/vm/src/exec-context/extended-tx.ts similarity index 100% rename from packages/vm/src/tx-context/extended-tx.ts rename to packages/vm/src/exec-context/extended-tx.ts diff --git a/packages/vm/src/tx-context/storage-tx-context.ts b/packages/vm/src/exec-context/storage-tx-context.ts similarity index 63% rename from packages/vm/src/tx-context/storage-tx-context.ts rename to packages/vm/src/exec-context/storage-tx-context.ts index 6eb9686e..b708bced 100644 --- a/packages/vm/src/tx-context/storage-tx-context.ts +++ b/packages/vm/src/exec-context/storage-tx-context.ts @@ -2,16 +2,21 @@ import {base16, Output, Pointer, PubKey} from "@aldea/core"; import {VM} from "../vm.js"; import {ExecutionError} from "../errors.js"; import {WasmContainer} from "../wasm-container.js"; -import {PkgData, Storage} from "../storage.js"; +import {MemStorage} from "../storage/mem-storage.js"; import {ExecContext} from "./exec-context.js"; +import {PkgData} from "../storage/pkg-data.js"; +/** + * TxContext based on a mem storage. Ideal for development. + * Not ideal for production where transactions are executed with inputs provided by users. + */ export class StorageTxContext implements ExecContext { private _txHash: Uint8Array - private storage: Storage + private storage: MemStorage private _signers: PubKey[] vm: VM - constructor (txHash: Uint8Array, signers: PubKey[], storage: Storage, vm: VM) { + constructor (txHash: Uint8Array, signers: PubKey[], storage: MemStorage, vm: VM) { this._txHash = txHash this.storage = storage this.vm = vm @@ -22,14 +27,14 @@ export class StorageTxContext implements ExecContext { return this._txHash } - stateByOutputId (id: Uint8Array): Output { - return this.storage.byOutputId(id).orElse(() => { + outputById (id: Uint8Array): Output { + return this.storage.outputByHash(id).orElse(() => { throw new ExecutionError(`output not present in utxo set: ${base16.encode(id)}`) }) } inputByOrigin (origin: Pointer): Output { - return this.storage.byOrigin(origin).orElse(() => { throw new ExecutionError(`unknown jig: ${origin.toString()}`)}) + return this.storage.outputByOrigin(origin).orElse(() => { throw new ExecutionError(`unknown jig: ${origin.toString()}`)}) } wasmFromPkgId (pkgId: string): WasmContainer { @@ -42,10 +47,6 @@ export class StorageTxContext implements ExecContext { return this.vm.compileSources(entries, sources) } - txId (): string { - return base16.encode(this._txHash) - } - signers (): PubKey[] { return this._signers } diff --git a/packages/vm/src/export-opts.ts b/packages/vm/src/exec-opts.ts similarity index 98% rename from packages/vm/src/export-opts.ts rename to packages/vm/src/exec-opts.ts index 092f5e32..67866afd 100644 --- a/packages/vm/src/export-opts.ts +++ b/packages/vm/src/exec-opts.ts @@ -1,3 +1,6 @@ +/** + * Options to tune tx execution. + */ export class ExecOpts { // Wasm execution wasmExecutionHydroSize: bigint diff --git a/packages/vm/src/execution-result.ts b/packages/vm/src/execution-result.ts index f0048e17..3853a380 100644 --- a/packages/vm/src/execution-result.ts +++ b/packages/vm/src/execution-result.ts @@ -2,9 +2,10 @@ import moment from "moment"; import {base16, Output} from "@aldea/core"; import {Abi} from "@aldea/core/abi"; import {ExecutionError} from "./errors.js"; -import {calculatePackageId} from "./calculate-package-id.js"; +import {calculatePackageHash} from "./calculate-package-hash.js"; import {Option} from "./support/option.js"; -import {PkgData} from "./storage.js"; + +import {PkgData} from "./storage/pkg-data.js"; export class PackageDeploy { sources: Map @@ -22,7 +23,7 @@ export class PackageDeploy { } get hash (): Uint8Array { - return calculatePackageId(this.entries, this.sources) + return calculatePackageHash(this.entries, this.sources) } get id (): string { diff --git a/packages/vm/src/index.ts b/packages/vm/src/index.ts index 0cc99682..0bc1c9ba 100644 --- a/packages/vm/src/index.ts +++ b/packages/vm/src/index.ts @@ -1,5 +1,8 @@ export * from './vm.js' -export * from './storage.js' -export * from './calculate-package-id.js' -export * from './tx-context/extended-tx.js' +export * from './storage/mem-storage.js' +export * from './calculate-package-hash.js' +export * from './exec-context/extended-tx.js' export * from './execution-result.js' +export {COIN_PKG_ID} from "./well-known-abi-nodes.js"; +export {Storage} from "./storage/generic-storage.js"; +export {PkgData} from "./storage/pkg-data.js"; diff --git a/packages/vm/src/jig-init-params.ts b/packages/vm/src/jig-init-params.ts index 4913be5a..7f595786 100644 --- a/packages/vm/src/jig-init-params.ts +++ b/packages/vm/src/jig-init-params.ts @@ -1,10 +1,13 @@ import {BufWriter, Pointer} from "@aldea/core"; import {Lock} from "./locks/lock.js"; import {AbiType} from "./memory/abi-helpers/abi-type.js"; -import {jigInitParamsTypeNode} from "./memory/well-known-abi-nodes.js"; +import {jigInitParamsTypeNode} from "./well-known-abi-nodes.js"; import {WasmContainer} from "./wasm-container.js"; import {WasmWord} from "./wasm-word.js"; +/** + * Simple data structure to lower the data needed to initialize a jig. + */ export class JigInitParams { origin: Pointer location: Pointer diff --git a/packages/vm/src/jig-ref.ts b/packages/vm/src/jig-ref.ts index dc938a8e..bad5fc7f 100644 --- a/packages/vm/src/jig-ref.ts +++ b/packages/vm/src/jig-ref.ts @@ -5,27 +5,52 @@ import {AbiClass} from "./memory/abi-helpers/abi-class.js"; import {WasmWord} from "./wasm-word.js"; import {AbiType} from "./memory/abi-helpers/abi-type.js"; +/** + * A reference to a value inside a wasm instance. + */ export class ContainerRef { + // Pointer to the value. ptr: WasmWord + // Type of the value ty: AbiType + // Container holding the value container: WasmContainer + /** + * @param {WasmWord} ptr - The pointer value. + * @param {AbiType} ty - The ABI type. + * @param {WasmContainer} container - The container object. + */ constructor (ptr: WasmWord, ty: AbiType, container: WasmContainer) { this.ptr = ptr this.ty = ty this.container = container } + /** + * Lifts the data from the container memory. Returns the data encoded in binary. + * + * @returns {Uint8Array} - The lifted data. + */ lift (): Uint8Array { return this.container.lifter.lift(this.ptr, this.ty) } + /** + * Compars if 2 values are the same. It compares for identity inside the container. + * this means that an object duplicated in memory is condered different by this method. + * (compares pointers, not objects) + * @param ref + */ equals (ref: ContainerRef) { return this.container.hash === ref.container.hash && this.ptr.equals(ref.ptr) } } +/** + * Class representing a live Jig Reference. + */ export class JigRef { ref: ContainerRef classIdx: number; @@ -34,6 +59,14 @@ export class JigRef { lock: Lock; readonly isNew: boolean; + /** + * @param {ContainerRef} ref - The ContainerRef object. + * @param {number} classIdx - The index of the class. + * @param {Pointer} origin - The origin Pointer object. + * @param {Pointer} latestLocation - The latestLocation Pointer object. + * @param {Lock} lock - The Lock object. + * @param {boolean} isNew - A boolean indicating if the instance is new. + */ constructor (ref: ContainerRef, classIdx: number, origin: Pointer, latestLocation: Pointer, lock: Lock, isNew: boolean) { this.ref = ref this.classIdx = classIdx @@ -43,10 +76,12 @@ export class JigRef { this.isNew = isNew } - get originBuf (): ArrayBuffer { - return this.origin.toBytes() - } - + /** + * Changes the lock of the jig. + * + * @param {Lock} newLock - The new lock to be set. + * @returns {void} + */ changeLock (newLock: Lock) { const container = this.ref.container const lockTy = AbiType.fromName('Lock'); @@ -55,41 +90,58 @@ export class JigRef { this.lock = newLock } + /** + * Name of the jig's class. + * @return {string} The name of the class. + */ className (): string { return this.classAbi().name } - outputObject (): any { - return { - origin: this.origin.toBytes(), - location: this.latestLocation.toBytes(), - classPtr: this.classPtr().toBytes() - } - } - + /** + * Return a Pointer identifying this jig class. + * + * @returns {Pointer} - The new Pointer instance. + */ classPtr (): Pointer { return new Pointer(this.ref.container.hash, this.classIdx) } - + /** + * Get the package where the jig is contained. + * + * @returns {WasmContainer} The package of the WasmContainer. + */ get package (): WasmContainer { return this.ref.container } + /** + * Retrieves the Abi Class Node for the jig referenced. + * + * @returns {AbiClass} The AbiClass object for the specified class index. + */ classAbi (): AbiClass { return this.ref.container.abi.exportedByIdx(this.classIdx).get().toAbiClass() } - static isJigRef (obj: Object): boolean { - // This is a little hack to avoid having issues when 2 different builds are used at the same time. - return obj instanceof JigRef || obj.constructor.name === 'JigRef' - } - + /** + * Extracts the entire state of the jig. + * + * @returns {Uint8Array} - The extracted properties as a Uint8Array. + */ extractProps (): Uint8Array { const wasm = this.ref.container return wasm.lifter.lift(this.ref.ptr, this.ref.ty) } + /** + * Retrieves the value of a property from the container. + * + * @param {string} propName - The name of the property to retrieve. + * + * @return {ContainerRef} - The container reference containing the property value. + */ getPropValue (propName: string): ContainerRef { const container = this.ref.container const abiClass = container.abi.exportedByIdx(this.classIdx).get().toAbiClass() diff --git a/packages/vm/src/locks/address-lock.ts b/packages/vm/src/locks/address-lock.ts index 2b543b5e..69e1d1ce 100644 --- a/packages/vm/src/locks/address-lock.ts +++ b/packages/vm/src/locks/address-lock.ts @@ -3,6 +3,9 @@ import {CoreLock, Lock} from './lock.js' import {TxExecution} from "../tx-execution.js"; import {PermissionError} from "../errors.js"; +/** + * A lock that can only be opened with the right signature. + */ export class AddressLock extends Lock { private addr: Address; diff --git a/packages/vm/src/locks/from-core-lock.ts b/packages/vm/src/locks/from-core-lock.ts index 8f0ad0c7..f8251523 100644 --- a/packages/vm/src/locks/from-core-lock.ts +++ b/packages/vm/src/locks/from-core-lock.ts @@ -6,6 +6,11 @@ import {OpenLock} from "./open-lock.js"; import {PublicLock} from "./public-lock.js"; import {FrozenLock} from "./frozen-lock.js"; +/** + * @aldea/core has a simpler data type for Locks. This is a helper function + * to convert from that class to the VM's Lock classes. + * @param lock + */ export function fromCoreLock (lock: CoreLock): Lock { switch (lock.type) { case LockType.ADDRESS: diff --git a/packages/vm/src/locks/frozen-lock.ts b/packages/vm/src/locks/frozen-lock.ts index be13ffde..62e16af4 100644 --- a/packages/vm/src/locks/frozen-lock.ts +++ b/packages/vm/src/locks/frozen-lock.ts @@ -3,6 +3,10 @@ import {LockType} from "@aldea/core"; import {TxExecution} from "../tx-execution.js"; import {PermissionError} from "../errors.js"; +/** + * A lock that can never be opened. A jig with this lock is considered out + * of the utxo set. + */ export class FrozenLock extends Lock { coreLock (): CoreLock { return new CoreLock(Number(LockType.FROZEN), new Uint8Array(0)); diff --git a/packages/vm/src/locks/jig-lock.ts b/packages/vm/src/locks/jig-lock.ts index 4c1abc8e..fece1736 100644 --- a/packages/vm/src/locks/jig-lock.ts +++ b/packages/vm/src/locks/jig-lock.ts @@ -3,6 +3,9 @@ import {Lock} from "./lock.js"; import {TxExecution} from "../tx-execution.js"; import {PermissionError} from "../errors.js"; +/** + * A lock that can only be opened by an specific jig. + */ export class JigLock extends Lock { private origin: Pointer; diff --git a/packages/vm/src/locks/lock.ts b/packages/vm/src/locks/lock.ts index c39c84be..abec2934 100644 --- a/packages/vm/src/locks/lock.ts +++ b/packages/vm/src/locks/lock.ts @@ -1,6 +1,16 @@ import {BufWriter, Lock as CoreLock} from "@aldea/core"; import {TxExecution} from "../tx-execution.js"; +/** + * Basic interface for a lock. + * In Aldea there are 5 types of lock: + * + * - `NoLock`: Provides no protection. Invalid at the end of a transaction. + * - `AddressLock`: A lock that can be opened with the right signature. + * - `JigLock`: A lock that can be opened only by the right jig. + * - `PublicLock`: A lock that can be opened by anyone. + * - `FrozenLock`: A lock that can never be opened. + */ export abstract class Lock { abstract coreLock (): CoreLock; @@ -18,6 +28,10 @@ export abstract class Lock { abstract canBeChanged (param: TxExecution): boolean; abstract canReceiveCalls (param: TxExecution): boolean; + + isOpen (): boolean { + return false + } } export { CoreLock } diff --git a/packages/vm/src/locks/open-lock.ts b/packages/vm/src/locks/open-lock.ts index 181f2242..826b4602 100644 --- a/packages/vm/src/locks/open-lock.ts +++ b/packages/vm/src/locks/open-lock.ts @@ -2,6 +2,9 @@ import {CoreLock, Lock} from "./lock.js"; import {LockType} from "@aldea/core"; import {TxExecution} from "../tx-execution.js"; +/** + * A lock that can be opened by anyone. + */ export class OpenLock extends Lock { coreLock (): CoreLock { return new CoreLock(LockType.NONE, new Uint8Array(0)); @@ -18,4 +21,8 @@ export class OpenLock extends Lock { canBeChanged (_param: TxExecution): boolean { return true; } + + isOpen (): boolean { + return true + } } diff --git a/packages/vm/src/memory-proxy.ts b/packages/vm/src/memory-proxy.ts index 19632751..c83e6504 100644 --- a/packages/vm/src/memory-proxy.ts +++ b/packages/vm/src/memory-proxy.ts @@ -1,26 +1,47 @@ import {WasmWord} from "./wasm-word.js"; import {BufReader} from "@aldea/core"; +import {ExecutionError} from "./errors.js"; export type TransferFn = (size: number) => void +/** + * MemoryProxy class provides methods for reading and writing data from/to WebAssembly memory + * in a nicer way. + * + * Reads and Writes are tracked by the vm, so the proxy executes a callback + * to let the vm know how much data was moved. + */ export class MemoryProxy { _mem: WebAssembly.Memory private onDataMoved: TransferFn; + /** + * Constructor for the class. + * + * @param {WebAssembly.Memory} mem - The WebAssembly memory object. + * @param {TransferFn} registerTransfer - Callback executed when data is moved in or out this memory. + */ constructor (mem: WebAssembly.Memory, registerTransfer: TransferFn) { this._mem = mem this.onDataMoved = registerTransfer } + /** + * Writes data to the WebAssembly memory starting from the specified pointer. + * + * @param {WasmWord} ptr - The pointer indicating the starting position in memory. + * @param {Uint8Array} data - The data to be written to memory. + * @throws {ExecutionError} If the memory pointer is less than 0 or the memory access is out of bounds. + */ write(ptr: WasmWord, data: Uint8Array) { this.onDataMoved(data.byteLength) let start = ptr.toInt() if (start < 0) { - throw new Error('Memory ptr should never be less than 0') + throw new ExecutionError('Memory ptr should never be less than 0') } let end = start + data.byteLength if (end > this._mem.buffer.byteLength) { - throw new Error(`Memory access out of bounds: ${end}`) + throw new ExecutionError(`Memory access out of bounds: ${end}`) } const view = new Uint8Array( @@ -31,15 +52,23 @@ export class MemoryProxy { view.set(data) } + /** + * Copies a buffer from memory. + * + * @param {WasmWord} ptr - The starting pointer of the memory to extract. + * @param {number} length - The length of the memory to extract. + * @returns {Uint8Array} - The extracted memory as a Uint8Array. + * @throws {ExecutionError} - If the memory pointer is less than 0 or the memory access is out of bounds. + */ extract(ptr: WasmWord, length: number): Uint8Array { this.onDataMoved(length) let start = ptr.toInt() if (start < 0) { - throw new Error('Memory ptr should never be less than 0') + throw new ExecutionError('Memory ptr should never be less than 0') } let end = start + length if (end > this._mem.buffer.byteLength) { - throw new Error(`Memory access out of bounds: ${end}`) + throw new ExecutionError(`Memory access out of bounds: ${end}`) } let response = new Uint8Array(length) @@ -47,11 +76,26 @@ export class MemoryProxy { return response } + /** + * Copies a buffer from memory and wraps into a reader. + * + * @param {WasmWord} ptr - The memory pointer to start reading from. + * @param {number} length - The number of bytes to read from the memory. + * @return {BufReader} - A BufReader object containing the read buffer. + */ read(ptr: WasmWord, length: number): BufReader { this.onDataMoved(length) return new BufReader(this.extract(ptr, length)) } + /** + * Reads a specific number from memory. This is used mainly for debug purposes. + * + * @param {number} n - The starting index of the memory block. + * @param {number} size - The size of the number to read (default is 1). + * @returns {number} - The read number. + * @throws {Error} - Throws an error if the size is not 1, 2, 4, or 8. + */ at(n: number, size: number = 1): number { const view = Buffer.from(new Uint8Array(this._mem.buffer, n, size)); if (size === 1) { diff --git a/packages/vm/src/memory.ts b/packages/vm/src/memory.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/vm/src/memory/abi-helpers/abi-access.ts b/packages/vm/src/memory/abi-helpers/abi-access.ts index b507abfb..9ef5311d 100644 --- a/packages/vm/src/memory/abi-helpers/abi-access.ts +++ b/packages/vm/src/memory/abi-helpers/abi-access.ts @@ -9,10 +9,15 @@ import { jigInitParamsAbiNode, lockAbiNode, outputAbiNode -} from "../well-known-abi-nodes.js"; +} from "../../well-known-abi-nodes.js"; import {AbiType} from "./abi-type.js"; import {AbiPlainObject} from "./abi-plain-object.js"; +/** + * The ABI structure is good to be stored, but it's not very convenient to work with. + * This is a re structured version of the ABI that is easier to work with, allowing searchs + * by different criteria and providing all the data needed together. + */ export class AbiAccess { readonly abi: Abi; private _exports: AbiExport[] diff --git a/packages/vm/src/memory/abi-helpers/abi-class.ts b/packages/vm/src/memory/abi-helpers/abi-class.ts index d87323fb..75431623 100644 --- a/packages/vm/src/memory/abi-helpers/abi-class.ts +++ b/packages/vm/src/memory/abi-helpers/abi-class.ts @@ -4,7 +4,7 @@ import {AbiType} from "./abi-type.js"; import {AbiField} from "./abi-plain-object.js"; import {WasmWord} from "../../wasm-word.js"; import {AbiMethod} from "./abi-method.js"; -import {lockTypeNode, outputTypeNode} from "../well-known-abi-nodes.js"; +import {lockTypeNode, outputTypeNode} from "../../well-known-abi-nodes.js"; import {ProxyDef} from "./proxy-def.js"; const BASE_FIELDS = [{ diff --git a/packages/vm/src/memory/abi-helpers/abi-imported-proxy.ts b/packages/vm/src/memory/abi-helpers/abi-imported-proxy.ts index dc1d166f..6767b8eb 100644 --- a/packages/vm/src/memory/abi-helpers/abi-imported-proxy.ts +++ b/packages/vm/src/memory/abi-helpers/abi-imported-proxy.ts @@ -1,6 +1,4 @@ import {Abi, AbiQuery, assertProxy, CodeKind, ProxyNode} from "@aldea/core/abi"; -import {AbiField} from "./abi-plain-object.js"; -import {basicJigAbiNode} from "../well-known-abi-nodes.js"; import {ProxyDef} from "./proxy-def.js"; export class AbiImportedProxy { diff --git a/packages/vm/src/memory/abi-helpers/abi-method.ts b/packages/vm/src/memory/abi-helpers/abi-method.ts index ca4d7829..6d1c7569 100644 --- a/packages/vm/src/memory/abi-helpers/abi-method.ts +++ b/packages/vm/src/memory/abi-helpers/abi-method.ts @@ -1,6 +1,6 @@ import {MethodNode} from "@aldea/core/abi"; import {Option} from "../../support/option.js"; -import {emptyTn} from "../well-known-abi-nodes.js"; +import {emptyTn} from "../../well-known-abi-nodes.js"; import {AbiType} from "./abi-type.js"; import {AbiArg} from "./abi-arg.js"; diff --git a/packages/vm/src/memory/abi-helpers/abi-type.ts b/packages/vm/src/memory/abi-helpers/abi-type.ts index ea7f6680..2d6bc1e0 100644 --- a/packages/vm/src/memory/abi-helpers/abi-type.ts +++ b/packages/vm/src/memory/abi-helpers/abi-type.ts @@ -1,5 +1,5 @@ import {normalizeTypeName, TypeNode} from "@aldea/core/abi"; -import {emptyTn} from "../well-known-abi-nodes.js"; +import {emptyTn} from "../../well-known-abi-nodes.js"; export class AbiType { private ty: TypeNode; @@ -45,6 +45,10 @@ export class AbiType { return this.fromName('ArrayBuffer') } + static string(): AbiType { + return this.fromName('string') + } + static u32(): AbiType { return this.fromName('u32') } diff --git a/packages/vm/src/memory/abi-helpers/proxy-def.ts b/packages/vm/src/memory/abi-helpers/proxy-def.ts index 3e0b0516..5d3e566d 100644 --- a/packages/vm/src/memory/abi-helpers/proxy-def.ts +++ b/packages/vm/src/memory/abi-helpers/proxy-def.ts @@ -1,5 +1,5 @@ import {AbiField} from "./abi-plain-object.js"; -import {basicJigAbiNode} from "../well-known-abi-nodes.js"; +import {basicJigAbiNode} from "../../well-known-abi-nodes.js"; export class ProxyDef { private _name: string; diff --git a/packages/vm/src/memory/bigint-buf.ts b/packages/vm/src/memory/bigint-buf.ts index d372c893..669163a3 100644 --- a/packages/vm/src/memory/bigint-buf.ts +++ b/packages/vm/src/memory/bigint-buf.ts @@ -1,3 +1,7 @@ +/** + * These are a series of helpers to deal with Aldea's Wasm bigints. + */ + const bigIntP = 28n const digitMask = (1n << bigIntP) - 1n diff --git a/packages/vm/src/memory/lower-value.ts b/packages/vm/src/memory/lower-value.ts index 9755bb81..834621b0 100644 --- a/packages/vm/src/memory/lower-value.ts +++ b/packages/vm/src/memory/lower-value.ts @@ -1,8 +1,8 @@ import {WasmContainer} from "../wasm-container.js"; import {WasmWord} from "../wasm-word.js"; -import {base16, BufReader, BufWriter, Pointer} from "@aldea/core"; +import {BufReader, BufWriter, Pointer} from "@aldea/core"; import {AbiType} from "./abi-helpers/abi-type.js"; -import {ARR_HEADER_LENGTH, BUF_RTID, STRING_RTID, TYPED_ARR_HEADER_LENGTH} from "./well-known-abi-nodes.js"; +import {ARR_HEADER_LENGTH, BUF_RTID, STRING_RTID, TYPED_ARR_HEADER_LENGTH} from "../well-known-abi-nodes.js"; import {CodeKind, normalizeTypeName} from "@aldea/core/abi"; import {Option} from "../support/option.js"; import {ExecutionError} from "../errors.js"; diff --git a/packages/vm/src/memory/value-lifter.ts b/packages/vm/src/memory/value-lifter.ts index 90960ac2..aa3afd22 100644 --- a/packages/vm/src/memory/value-lifter.ts +++ b/packages/vm/src/memory/value-lifter.ts @@ -4,7 +4,7 @@ import {base16, BufReader, BufWriter} from "@aldea/core"; import {AbiType} from "./abi-helpers/abi-type.js"; import {AbiPlainObject} from "./abi-helpers/abi-plain-object.js"; import {AbiClass} from "./abi-helpers/abi-class.js"; -import {BUF_RTID} from "./well-known-abi-nodes.js"; +import {BUF_RTID} from "../well-known-abi-nodes.js"; import {ExecutionError} from "../errors.js"; import {CodeKind} from "@aldea/core/abi"; import {digitsToBigInt} from "./bigint-buf.js"; diff --git a/packages/vm/src/metering/hydro-counter.ts b/packages/vm/src/metering/hydro-counter.ts new file mode 100644 index 00000000..277125e8 --- /dev/null +++ b/packages/vm/src/metering/hydro-counter.ts @@ -0,0 +1,58 @@ +import {ExecutionError} from "../errors.js"; + +/** + * Hydros are discrete units of work that are measuered and used to charge for transaction. + * A Hydro is always an integer amount. They cannot be fractioned, but they are charged every certain number of events + * + * For example, if the cost of 1000 bytes moved between containers is one hydro then: + * - 100 bytes moved -> 1 Hydro + * - 3000 bytes moved -> 3 Hydros + * - 4500 bytes moved -> 5 Hydros + * - 0 bytes moved -> 0 Hydros + * + * This class helps to keep track of the hydros consumed by a certain operation. + */ +export class HydroCounter { + private tag: string; + private total: bigint + private count: bigint + private hydroSize: bigint + hydros: bigint + private maxHydros: bigint; + + constructor (tag: string, hydroSize: bigint, maxHydros: bigint) { + this.tag = tag + this.total = 0n + this.count = 0n + this.hydros = 0n + this.hydroSize = hydroSize + this.maxHydros = maxHydros + } + + add (amount: bigint) { + this.total += amount + this.count += amount + const newHydros = this.count / this.hydroSize + this.hydros += newHydros + this.count = this.count % this.hydroSize + + if (this.hydros > this.maxHydros) { + throw new ExecutionError(`Max hydros for ${this.tag} (${this.maxHydros}) was over passed`) + } + } + + clear (): number { + let res = Number(this.hydros) + if (this.count > 0n) { + res += 1 + } + this.total = 0n + this.count = 0n + this.hydros = 0n + return res + } + + inc () { + this.add(1n) + } +} diff --git a/packages/vm/src/metering/measurements.ts b/packages/vm/src/metering/measurements.ts new file mode 100644 index 00000000..a6f60624 --- /dev/null +++ b/packages/vm/src/metering/measurements.ts @@ -0,0 +1,36 @@ +import {HydroCounter} from "./hydro-counter.js"; +import {ExecOpts} from "../exec-opts.js"; + +/** + * Data structure with all the measurements taken during the execution of a transaction to + * calculate the execution cost. + */ +export class Measurements { + movedData: HydroCounter; + wasmExecuted: HydroCounter; + numContainers: HydroCounter; + numSigs: HydroCounter; + originChecks: HydroCounter; + newJigs: HydroCounter; + deploys: HydroCounter; + + constructor (opts: ExecOpts) { + this.movedData = new HydroCounter('Moved Data', opts.moveDataHydroSize, opts.moveDataMaxHydros) + this.wasmExecuted = new HydroCounter('Raw Execution', opts.wasmExecutionHydroSize, opts.wasmExecutionMaxHydros) + this.numContainers = new HydroCounter('Num Containers', opts.numContHydroSize, opts.numContMaxHydros) + this.numSigs = new HydroCounter('Num Sigs', opts.numSigsHydroSize, opts.numSigsMaxHydros) + this.originChecks = new HydroCounter('Load By Origin', opts.originCheckHydroSize, opts.originCheckMaxHydros) + this.newJigs = new HydroCounter('New Jigs', opts.newJigHydroSize, opts.newJigMaxHydros) + this.deploys = new HydroCounter('Deploys', 1n, 30000n) + } + + clear () { + return this.movedData.clear() + + this.wasmExecuted.clear() + + this.numContainers.clear() + + this.numSigs.clear() + + this.originChecks.clear() + + this.newJigs.clear() + + this.deploys.clear(); + } +} diff --git a/packages/vm/src/statement-result.ts b/packages/vm/src/statement-result.ts index 300b3275..0ab568d9 100644 --- a/packages/vm/src/statement-result.ts +++ b/packages/vm/src/statement-result.ts @@ -4,6 +4,11 @@ import {WasmContainer} from "./wasm-container.js"; import {AbiType} from "./memory/abi-helpers/abi-type.js"; import {WasmWord} from "./wasm-word.js"; +/** + * Every opcode execution returns a statement result. + * This is the abstract class for all statement results. + * @abstract + */ export abstract class StatementResult { private _idx: number constructor(idx: number) { @@ -19,6 +24,11 @@ export abstract class StatementResult { } } +/** + * A statement result for an `IMPORT` or `DEPLOY` statements. + * It contains a wasm instance. + * @extends StatementResult + */ export class WasmStatementResult extends StatementResult { private readonly _instance: WasmContainer; @@ -36,6 +46,17 @@ export class WasmStatementResult extends StatementResult { } } +/** + * A statement result for: + * - `LOAD` + * - `LOAD_BY_ORIGIN` + * - `NEW` + * - `CALL` + * - `EXEC` + * + * It contains a pointer to a value inside a wasm instance. + * @extends StatementResult + */ export class ValueStatementResult extends StatementResult { value: ContainerRef @@ -58,6 +79,14 @@ export class ValueStatementResult extends StatementResult { } } +/** + * Represents an empty statement result. + * This is the result for executing the following opcodes: + * - `LOCK` + * - `FUND` + * - `SIGN` + * - `SIGN_TO` + */ export class EmptyStatementResult extends StatementResult { asContainer (): WasmContainer { throw new Error('not a container'); diff --git a/packages/vm/src/storage.ts b/packages/vm/src/storage.ts deleted file mode 100644 index 3cdec880..00000000 --- a/packages/vm/src/storage.ts +++ /dev/null @@ -1,206 +0,0 @@ -import {Address, base16, LockType, Output, Pointer, Tx} from "@aldea/core"; -import {Abi} from "@aldea/core/abi"; -import {ExecutionResult, PackageDeploy} from "./execution-result.js"; -import {WasmContainer} from "./wasm-container.js"; -import {Option} from "./support/option.js"; - -export class PkgData { - abi: Abi - docs: Uint8Array - entries: string[] - id: Uint8Array - mod: WebAssembly.Module - sources: Map - wasmBin: Uint8Array - - constructor( - abi: Abi, - docs: Uint8Array, - entries: string[], - id: Uint8Array, - mod: WebAssembly.Module, - sources: Map, - wasmBin: Uint8Array - ) { - this.abi = abi - this.docs = docs - this.entries = entries - this.id = id - this.mod = mod - this.sources = sources - this.wasmBin = wasmBin - } - - static fromPackageDeploy(deploy: PackageDeploy) { - return new this( - deploy.abi, - deploy.docs, - deploy.entries, - deploy.hash, - new WebAssembly.Module(deploy.bytecode), - deploy.sources, - deploy.bytecode - ) - } -} - -export class Storage { - private utxosByOid: Map // output_id -> state. Only utxos - private utxosByAddress: Map // address -> state. Only utxos - private utxosByLock: Map // address -> state. Only utxos - private tips: Map // origin -> latest output_id - private origins: Map // utxo -> origin. Only utxos - private txs: Map - private execResults: Map // txid -> transaction execution. - private packages: Map // pkg_id -> pkg data - private historicalUtxos: Map - - constructor() { - this.utxosByOid = new Map() - this.tips = new Map() - this.origins = new Map() - this.txs = new Map() - this.execResults = new Map() - this.packages = new Map() - this.historicalUtxos = new Map() - this.utxosByAddress = new Map() - this.utxosByLock = new Map() - } - - - persistTx(tx: Tx) { - this.txs.set(tx.id, tx) - } - - persistExecResult(txExecution: ExecutionResult) { - this.execResults.set(txExecution.txId, txExecution) - txExecution.outputs.forEach((state) => this.addUtxo(state)) - txExecution.deploys.forEach(pkgDeploy => this.addPackage(pkgDeploy.hash, PkgData.fromPackageDeploy(pkgDeploy))) - } - - private isNew(o: Output): boolean { - return o.origin.equals(o.location) - } - - private addressFor(o: Output): Option
{ - return Option.some(o) - .filter(o => o.lock.type === LockType.ADDRESS) - .map(o => new Address(o.lock.data)) - } - - addUtxo(output: Output) { - const currentLocation = output.id; - const originStr = output.origin.toString(); - - if (!this.isNew(output)) { - const prevLocation = this.tips.get(originStr) - - // if it's not new but there is not prev location that means that is a local client. It's fine. - if (prevLocation) { - const oldOutput = this.utxosByOid.get(prevLocation) - this.utxosByOid.delete(prevLocation) - this.tips.delete(originStr) - this.origins.delete(originStr) - if (oldOutput) { - this.addressFor(oldOutput).ifPresent((addr) => { - const list = this.utxosByAddress.get(addr.toString()) - if (!list) { - throw new Error('error') - } - const filtered = list.filter(s => !s.origin.equals(oldOutput.origin)) - this.utxosByAddress.set(addr.toString(), filtered) - }) - - const list = this.utxosForLock(oldOutput.lock.toHex()) - const filtered = list.filter(s => !s.origin.equals(oldOutput.origin)) - this.utxosByLock.set(oldOutput.lock.toHex(), filtered) - } - } - } - - this.utxosByOid.set(currentLocation, output) - this.historicalUtxos.set(currentLocation, output) - this.tips.set(originStr, currentLocation) - this.origins.set(currentLocation, originStr) - if (output.lock.type === LockType.ADDRESS) { - const address = this.addressFor(output).map(a => a.toString()).get(); - const previous = this.utxosByAddress.get(address) - if (previous) { - previous.push(output) - } else { - this.utxosByAddress.set(address, [output]) - } - } - const byLock = this.utxosForLock(output.lock.toHex()) - byLock.push(output) - this.utxosByLock.set(output.lock.toHex(), byLock) - } - - getJigStateByOrigin(origin: Pointer): Option { - const latestLocation = this.tips.get(origin.toString()) - if (!latestLocation) return Option.none() - const ret = this.utxosByOid.get(latestLocation) - return Option.fromNullable(ret) - } - - getJigStateByOutputId (outputId: Uint8Array): Option { - const state = this.utxosByOid.get(base16.encode(outputId)) - return Option.fromNullable(state) - } - - tipFor(origin: Pointer): Uint8Array { - const tip = this.tips.get(origin.toString()); - if (!tip) throw new Error('not found') - return base16.decode(tip) - } - - getTx(txid: string): Option { - return Option.fromNullable(this.txs.get(txid)) - } - - getExecResult(txid: string): Option { - return Option.fromNullable(this.execResults.get(txid)) - } - - addPackage(id: Uint8Array, pkgData: PkgData): void { - this.packages.set(base16.encode(id), pkgData) - } - - getPkg (id: string): Option { - const pkg = this.packages.get(id) - return Option.fromNullable(pkg) - } - - hasModule(id: Uint8Array): boolean { - return this.packages.has(base16.encode(id)); - } - - getHistoricalUtxo (outputId: Uint8Array): Option { - const state = this.historicalUtxos.get(base16.encode(outputId)) - return Option.fromNullable(state) - } - - utxosForAddress(userAddr: Address): Output[] { - return Option.fromNullable(this.utxosByAddress.get(userAddr.toString())) - .orDefault([]) - } - - utxosForLock(lockHex: string): Output[] { - return Option.fromNullable(this.utxosByLock.get(lockHex)) - .orDefault([]) - } - - byOutputId(id: Uint8Array): Option { - return this.getJigStateByOutputId(id); - } - - byOrigin(origin: Pointer): Option { - return this.getJigStateByOrigin(origin); - } - - wasmForPackageId(moduleId: string): Option { - return this.getPkg(moduleId).map(pkg => { - return new WasmContainer(pkg.mod, pkg.abi, pkg.id); - }) - } -} diff --git a/packages/vm/src/storage/generic-storage.ts b/packages/vm/src/storage/generic-storage.ts new file mode 100644 index 00000000..a681484b --- /dev/null +++ b/packages/vm/src/storage/generic-storage.ts @@ -0,0 +1,77 @@ +import {Address, Output, Pointer, Tx} from "@aldea/core"; +import {ExecutionResult} from "../execution-result.js"; +import {Option} from "../support/option.js"; +import {WasmContainer} from "../wasm-container.js"; + +import {PkgData} from "./pkg-data.js"; + +/** + * An interface for a generic storage object. + */ +export interface Storage { + /** + * Persists a transaction. + * + * @param {Tx} tx - The transaction to persist. + * + * @return {void} + */ + persistTx (tx: Tx): Promise; + + /** + * Persists the execution result of a transaction. + * + * @param {ExecutionResult} txExecution - The execution result of the transaction. + * @return {Promise} - A promise that resolves when the execution result is persisted successfully. + */ + persistExecResult (txExecution: ExecutionResult): Promise; + + /** + * Persiste a new UTXO updating all needed indexes + * + * @param output - The output to add as a UTXO. + * @returns A Promise that resolves when the UTXO has been added successfully. + */ + addUtxo (output: Output): Promise; + + /** + * Retrieves the output id of the tip of the jig with the given origin. + * + * @param {Pointer} origin - The origin to retrieve the tip for. + * @returns {Option} - An option containing the tip if found, otherwise None. + */ + tipFor (origin: Pointer): Option; + + // TODO: Make async + getTx (txid: string): Option; + + // TODO: Make async + getExecResult (txid: string): Option; + + // TODO: Make async + addPackage (id: Uint8Array, pkgData: PkgData): void; + + // TODO: Make async + getPkg (id: string): Option; + + // TODO: Make async + getHistoricalUtxo (outputId: Uint8Array): Option; + + // TODO: Make async + utxosForAddress (userAddr: Address): Output[]; + + // TODO: Make async + utxosForLock (lockHex: string): Output[]; + + // TODO: Make async + outputByHash (hash: Uint8Array): Option; + + // TODO: Make async + outputById (id: string): Option; + + // TODO: Make async + outputByOrigin (origin: Pointer): Option; + + // TODO: Make async + wasmForPackageId (moduleId: string): Option; +} diff --git a/packages/vm/src/storage/mem-storage.ts b/packages/vm/src/storage/mem-storage.ts new file mode 100644 index 00000000..02dcd440 --- /dev/null +++ b/packages/vm/src/storage/mem-storage.ts @@ -0,0 +1,158 @@ +import {Address, base16, LockType, Output, Pointer, Tx} from "@aldea/core"; +import {ExecutionResult} from "../execution-result.js"; +import {WasmContainer} from "../wasm-container.js"; +import {Option} from "../support/option.js"; +import {Storage} from "./generic-storage.js"; +import {PkgData} from "./pkg-data.js"; + +/** + * Full in-memory implementation of the storage. + */ +export class MemStorage implements Storage { + private utxosByOutputId: Map // output_id -> state. Only utxos + private utxosByAddress: Map // address -> state. Only utxos + private utxosByLock: Map // address -> state. Only utxos + private tips: Map // origin -> latest output_id + private txs: Map + private execResults: Map // txid -> transaction execution. + private packages: Map // pkg_id -> pkg data + private historicalUtxos: Map + + constructor() { + this.utxosByOutputId = new Map() + this.tips = new Map() + this.txs = new Map() + this.execResults = new Map() + this.packages = new Map() + this.historicalUtxos = new Map() + this.utxosByAddress = new Map() + this.utxosByLock = new Map() + } + + + /** + * Persists a transaction. + * + * @param {Tx} tx - The transaction to persist. + * + * @return {void} + */ + async persistTx (tx: Tx): Promise { + this.txs.set(tx.id, tx) + } + + async persistExecResult (txExecution: ExecutionResult): Promise { + this.execResults.set(txExecution.txId, txExecution) + await Promise.all(txExecution.outputs.map((state) => this.addUtxo(state))) + txExecution.deploys.forEach(pkgDeploy => this.addPackage(pkgDeploy.hash, PkgData.fromPackageDeploy(pkgDeploy))) + } + + async addUtxo (output: Output): Promise { + const currentOutputId = output.id; + const originStr = output.origin.toString(); + const origin = Pointer.fromString(originStr) + + const prevOutput = this.tipFor(origin) + + prevOutput.ifPresent((prevOutputId) => { + const prevOutput = this.outputById(prevOutputId).get() + this.utxosByOutputId.delete(prevOutputId) + + if (prevOutput.lock.type === LockType.ADDRESS) { + const address = new Address(prevOutput.lock.data); + const previousByAddr = this.utxosForAddress(address) + .filter(o => o.id !== prevOutputId) + this.utxosByAddress.set(address.toString(), previousByAddr) + } + const serializedLock = prevOutput.lock.toHex(); + const previousByLock = this.utxosForLock(serializedLock) + .filter(o => o.id !== prevOutputId) + this.utxosByAddress.set(serializedLock, previousByLock) + }) + + if (output.lock.type !== LockType.FROZEN) { + this.utxosByOutputId.set(currentOutputId, output) + } + + if (output.lock.type === LockType.ADDRESS) { + const address = new Address(output.lock.data); + const previousByAddr = this.utxosForAddress(address) + previousByAddr.push(output) + this.utxosByAddress.set(address.toString(), previousByAddr) + } + + const serializedLock = output.lock.toHex(); + const previousByLock = this.utxosForLock(serializedLock) + previousByLock.push(output) + this.utxosByAddress.set(serializedLock, previousByLock) + + this.tips.set(originStr, currentOutputId) + this.historicalUtxos.set(currentOutputId, output) + } + + /** + * Retrieves the output id of the tip of the jig with the given origin. + * + * @param {Pointer} origin - The origin to retrieve the tip for. + * @returns {Option} - An option containing the tip if found, otherwise None. + */ + tipFor (origin: Pointer): Option { + const tip = this.tips.get(origin.toString()); + return Option.fromNullable(tip) + } + + getTx (txid: string): Option { + return Option.fromNullable(this.txs.get(txid)) + } + + getExecResult (txid: string): Option { + return Option.fromNullable(this.execResults.get(txid)) + } + + addPackage (id: Uint8Array, pkgData: PkgData): void { + this.packages.set(base16.encode(id), pkgData) + } + + getPkg (id: string): Option { + const pkg = this.packages.get(id) + return Option.fromNullable(pkg) + } + + getHistoricalUtxo (outputId: Uint8Array): Option { + const state = this.historicalUtxos.get(base16.encode(outputId)) + return Option.fromNullable(state) + } + + utxosForAddress (userAddr: Address): Output[] { + return Option.fromNullable(this.utxosByAddress.get(userAddr.toString())) + .orDefault([]) + } + + utxosForLock (lockHex: string): Output[] { + return Option.fromNullable(this.utxosByLock.get(lockHex)) + .orDefault([]) + } + + outputByHash (hash: Uint8Array): Option { + const id = base16.encode(hash) + return this.outputById(id) + } + + outputById (id: string): Option { + const state = this.utxosByOutputId.get(id) + return Option.fromNullable(state) + } + + outputByOrigin (origin: Pointer): Option { + const latestLocation = this.tips.get(origin.toString()) + if (!latestLocation) return Option.none() + const ret = this.utxosByOutputId.get(latestLocation) + return Option.fromNullable(ret) + } + + wasmForPackageId (moduleId: string): Option { + return this.getPkg(moduleId).map(pkg => { + return new WasmContainer(pkg.mod, pkg.abi, pkg.id); + }) + } +} diff --git a/packages/vm/src/storage/pkg-data.ts b/packages/vm/src/storage/pkg-data.ts new file mode 100644 index 00000000..d59e6bc9 --- /dev/null +++ b/packages/vm/src/storage/pkg-data.ts @@ -0,0 +1,45 @@ +import {Abi} from "@aldea/core/abi"; +import {PackageDeploy} from "../execution-result.js"; + +/** + * All Data related to a package. Sources + everything creted by the compiler. + */ +export class PkgData { + abi: Abi + docs: Uint8Array + entries: string[] + id: Uint8Array + mod: WebAssembly.Module + sources: Map + wasmBin: Uint8Array + + constructor ( + abi: Abi, + docs: Uint8Array, + entries: string[], + id: Uint8Array, + mod: WebAssembly.Module, + sources: Map, + wasmBin: Uint8Array + ) { + this.abi = abi + this.docs = docs + this.entries = entries + this.id = id + this.mod = mod + this.sources = sources + this.wasmBin = wasmBin + } + + static fromPackageDeploy (deploy: PackageDeploy) { + return new this( + deploy.abi, + deploy.docs, + deploy.entries, + deploy.hash, + new WebAssembly.Module(deploy.bytecode), + deploy.sources, + deploy.bytecode + ) + } +} diff --git a/packages/vm/src/tx-context/exec-context.ts b/packages/vm/src/tx-context/exec-context.ts deleted file mode 100644 index 27e4e668..00000000 --- a/packages/vm/src/tx-context/exec-context.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {Output, Pointer, PubKey} from "@aldea/core"; -import {WasmContainer} from "../wasm-container.js"; -import {PkgData} from "../storage.js"; - -export interface ExecContext { - txHash (): Uint8Array - - stateByOutputId (id: Uint8Array): Output - - inputByOrigin (origin: Pointer): Output - - wasmFromPkgId (pkgId: string): WasmContainer - - compile (entries: string[], sources: Map): Promise - - txId (): string - - signers (): PubKey[] -} - diff --git a/packages/vm/src/tx-execution.ts b/packages/vm/src/tx-execution.ts index 447792ea..589f7e0c 100644 --- a/packages/vm/src/tx-execution.ts +++ b/packages/vm/src/tx-execution.ts @@ -1,13 +1,12 @@ import {ContainerRef, JigRef} from "./jig-ref.js" -import {ExecutionError, IvariantBroken} from "./errors.js" +import {ExecutionError, InvariantBroken, PermissionError} from "./errors.js" import {OpenLock} from "./locks/open-lock.js" import {AuthCheck, WasmContainer} from "./wasm-container.js"; import {Address, base16, BufReader, BufWriter, Lock as CoreLock, LockType, Output, Pointer} from '@aldea/core'; -import {COIN_CLS_PTR, outputTypeNode} from "./memory/well-known-abi-nodes.js"; +import {COIN_CLS_PTR, outputTypeNode} from "./well-known-abi-nodes.js"; import {ExecutionResult, PackageDeploy} from "./execution-result.js"; import {EmptyStatementResult, StatementResult, ValueStatementResult, WasmStatementResult} from "./statement-result.js"; -import {ExecContext} from "./tx-context/exec-context.js"; -import {PkgData} from "./storage.js"; +import {ExecContext} from "./exec-context/exec-context.js"; import {JigData} from "./memory/lower-value.js"; import {Option} from "./support/option.js"; import {WasmArg, WasmWord} from "./wasm-word.js"; @@ -18,102 +17,73 @@ import {AddressLock} from "./locks/address-lock.js"; import {FrozenLock} from "./locks/frozen-lock.js"; import {AbiMethod} from "./memory/abi-helpers/abi-method.js"; import {JigInitParams} from "./jig-init-params.js"; -import {ArgsTranslator} from "./args-translator.js"; +import {ArgumentsPreProcessor} from "./arguments-pre-processor.js"; import {CodeKind} from "@aldea/core/abi"; import {AbiArg} from "./memory/abi-helpers/abi-arg.js"; -import {ExecOpts} from "./export-opts.js"; +import {ExecOpts} from "./exec-opts.js"; +import {Measurements} from "./metering/measurements.js"; +import {PkgData} from "./storage/pkg-data.js"; export const MIN_FUND_AMOUNT = 100 -export class DiscretCounter { - private tag: string; - private total: bigint - private count: bigint - private hydroSize: bigint - hydros: bigint - private maxHydros: bigint; - - constructor (tag: string, hydroSize: bigint, maxHydros: bigint) { - this.tag = tag - this.total = 0n - this.count = 0n - this.hydros = 0n - this.hydroSize = hydroSize - this.maxHydros = maxHydros - } - - add(amount: bigint) { - this.total += amount - this.count += amount - const newHydros = this.count / this.hydroSize - this.hydros += newHydros - this.count = this.count % this.hydroSize - - if (this.hydros > this.maxHydros) { - throw new ExecutionError(`Max hydros for ${this.tag} (${this.maxHydros}) was over passed`) - } - } - - clear (): number { - let res = Number(this.hydros) - if (this.count > 0n) { - res +=1 - } - this.total = 0n - this.count = 0n - this.hydros = 0n - return res - } - - inc () { - this.add(1n) - } -} - - -export class Measurements { - movedData: DiscretCounter; - wasmExecuted: DiscretCounter; - numContainers: DiscretCounter; - numSigs: DiscretCounter; - originChecks: DiscretCounter; - newJigs: DiscretCounter; - deploys: DiscretCounter; - - constructor (opts: ExecOpts) { - this.movedData = new DiscretCounter('Moved Data', opts.moveDataHydroSize, opts.moveDataMaxHydros) - this.wasmExecuted = new DiscretCounter('Raw Execution', opts.wasmExecutionHydroSize, opts.wasmExecutionMaxHydros) - this.numContainers = new DiscretCounter('Num Containers', opts.numContHydroSize, opts.numContMaxHydros) - this.numSigs = new DiscretCounter('Num Sigs', opts.numSigsHydroSize, opts.numSigsMaxHydros) - this.originChecks = new DiscretCounter('Load By Origin', opts.originCheckHydroSize, opts.originCheckMaxHydros) - this.newJigs = new DiscretCounter('New Jigs', opts.newJigHydroSize, opts.newJigMaxHydros) - this.deploys = new DiscretCounter('Deploys', 1n, 30000n) - } - - clear () { - return this.movedData.clear() + - this.wasmExecuted.clear() + - this.numContainers.clear() + - this.numSigs.clear() + - this.originChecks.clear() + - this.newJigs.clear() + - this.deploys.clear(); - } -} +/** + * Class representing the execution of a transaction. + * The main methods are 1 to 1 relation with Aldea opcodes. + * This class is in charge of coordinate all the containers being used, + * validate the interactions between jigs and keep track of all the data + * produced by the execution. + * + * @class + * @param {ExecContext} context - The execution context of the transaction. + * @param {ExecOpts} opts - The execution options for the transaction. + */ class TxExecution { + /* This object provides all the data needed from outside to make the execution work */ execContext: ExecContext; + + /* Jigs that were hydrated during the execution of the current tx */ private jigs: JigRef[]; + + /* Containers being used by the tx. */ private wasms: Map; + + /* Permission stack. Keeps track of which jig is executing at a given moment. */ private readonly stack: Pointer[]; + + /* New packages deployed in the current tx. */ deployments: PkgData[]; + + /* The result of each statement (opcode) executed in the current tx */ statements: StatementResult[] + + /* How many coins where used to fund the tx at a given moment */ private fundAmount: number; + + /** + * Jigs that were affected by the curret tx. An affected jig is a method + * that requires a new location at the end of the transaction. Which means that + * it's a new jig or it received a method call from the top level or from + * another jig. + * @private + */ private affectedJigs: JigRef[] + + /* Auxiliary data to keep jig creations in sync */ private nextOrigin: Option + + /* Options for the current execution */ private opts: ExecOpts + + /* Measurements used to calculate hydro (gas) usage. */ private measurements: Measurements; + /** + * Constructor for the class. + * + * @param {ExecContext} context - The execution context. + * @param {ExecOpts} opts - The execution options. + */ constructor (context: ExecContext, opts: ExecOpts) { this.execContext = context this.jigs = [] @@ -129,106 +99,32 @@ class TxExecution { this.measurements.numSigs.add(BigInt(this.execContext.signers().length)) } - finalize (): ExecutionResult { - const result = new ExecutionResult(this.execContext.txId()) - if (this.fundAmount < MIN_FUND_AMOUNT) { - throw new ExecutionError(`Not enough funding. Provided: ${this.fundAmount}. Needed: ${MIN_FUND_AMOUNT}`) - } - // this.jigs.forEach(jigRef => { - // if (jigRef.lock.isOpen()) { - // throw new PermissionError(`Finishing tx with unlocked jig (${jigRef.className()}): ${jigRef.origin}`) - // } - // }) - // - this.affectedJigs.forEach((jigRef, index) => { - const origin = jigRef.origin - const location = new Pointer(this.execContext.txHash(), index) - const jigProps = jigRef.extractProps() - const jigState = new Output( - origin, - location, - jigRef.classPtr(), - jigRef.lock.coreLock(), - jigProps, - jigRef.ref.container.abi.abi - ) - result.addOutput(jigState) - - if (!jigRef.isNew) { - result.addSpend(this.execContext.inputByOrigin(jigRef.origin)) - } - }) - - this.jigs.forEach(jig => { - if (!this.affectedJigs.includes(jig)) { - result.addRead(this.execContext.inputByOrigin(jig.origin)) - } - }) - - this.deployments.forEach(pkgData => { - result.addDeploy(new PackageDeploy( - pkgData.sources, - pkgData.entries, - pkgData.wasmBin, - pkgData.abi, - pkgData.docs - )) - }) - - for (const wasm of this.wasms.values()) { - wasm.clearExecution() - } - this.wasms = new Map() - this.jigs = [] - this.statements = [] - result.setHydrosUsed(this.measurements.clear()) - result.finish() - return result - } + // ------- + // Opcodes + // ------- /** - * Opcodes + * Executes an `IMPORT` operation. Imports a module specified by the given module ID. + * + * @param {Uint8Array} modId - The module ID to import. + * @return {StatementResult} - The statement result. */ - - fund (coinIdx: number): StatementResult { - const coinJig = this.jigAt(coinIdx) - if (!coinJig.classPtr().equals(COIN_CLS_PTR)) { - throw new ExecutionError(`Not a coin: ${coinJig.origin}`) - } - - coinJig.lock.assertOpen(this) - - const amount = coinJig.getPropValue('amount').ptr - this.fundAmount += amount.toUInt() - coinJig.changeLock(new FrozenLock()) - const stmt = new EmptyStatementResult(this.statements.length) - this.statements.push(stmt) - this.marKJigAsAffected(coinJig) - return stmt - } - - call (jigIdx: number, methodIdx: number, argsBuf: Uint8Array): StatementResult { - const jig = this.jigAt(jigIdx) - jig.lock.assertOpen(this) - const abiClass = jig.classAbi(); - const method = abiClass.methodByIdx(methodIdx).get() - - const wasm = jig.ref.container; - const args = this.translateAndLowerArgs(wasm, method.args, argsBuf); - - this.marKJigAsAffected(jig) - const res = this.performMethodCall(jig, method, args) - - let stmt: StatementResult = res.map(ref => { - return new ValueStatementResult(this.statements.length, ref.ty, ref.ptr, ref.container) - }).orElse(() => new EmptyStatementResult(this.statements.length)) - - this.statements.push(stmt) - return stmt + import (modId: Uint8Array): StatementResult { + const instance = this.assertContainer(base16.encode(modId)) + const ret = new WasmStatementResult(this.statements.length, instance); + this.statements.push(ret) + return ret } + /** + * Executes a `LOAD` operation. Loads a jig from the context using the output id as key. + * If needed it hydrates a container for it. + * + * @param {Uint8Array} outputId - The ID of the output to load. + * @return {StatementResult} - The loaded output as a StatementResult object. + */ load (outputId: Uint8Array): StatementResult { - const output = this.execContext.stateByOutputId(outputId) + const output = this.execContext.outputById(outputId) const jigRef = this.hydrate(output) const ret = new ValueStatementResult(this.statements.length, jigRef.ref.ty.proxy(), jigRef.ref.ptr, jigRef.ref.container); @@ -236,6 +132,14 @@ class TxExecution { return ret } + /** + * Executes a `LOADBYORIGIN` operation. Loads a Jig from context by its origin. If needed it hydrates the jig + * container. + * + * @param {Uint8Array} originBytes - The origin bytes of the statement. + * + * @returns {StatementResult} - The loaded statement result. + */ loadByOrigin (originBytes: Uint8Array): StatementResult { this.measurements.originChecks.inc() const origin = Pointer.fromBytes(originBytes) @@ -246,6 +150,17 @@ class TxExecution { return stmt } + /** + * Executes a `NEW` operation. Searches in the package at the statement `statementIndex`, + * and looks for a class at the index `classIdx`. Then creates an instance using the provided + * arguments. + * + * @param {number} statementIndex - The index of the statement in the statements array. + * @param {number} classIdx - The index of the class in the wasm ABI. + * @param {Uint8Array} argsBuf - Arguments encoded in Aldea Format. + * @returns {StatementResult} - Reference to the created instance. + * @throws {ExecutionError} - If the instantiated jig is not found. + */ instantiate (statementIndex: number, classIdx: number, argsBuf: Uint8Array): StatementResult { const statement = this.statements[statementIndex] const wasm = statement.asContainer() @@ -265,7 +180,7 @@ class TxExecution { const jigRef = this.jigs.find(j => j.package === wasm && j.ref.ptr.equals(result)) if (!jigRef) { - throw new Error('jig should had been created created') + throw new ExecutionError('jig should had been created created') } const ret = new ValueStatementResult(this.statements.length, method.rtype, result, wasm); @@ -273,6 +188,45 @@ class TxExecution { return ret } + /** + * Executes a `CALL` operation. Call a method on a Jig instance. It sends a message + * to the jig contained at `jigIdx` statement index. The message is defined by `methodIdx`. + * + * @param {number} jigIdx - The index of the Jig instance. + * @param {number} methodIdx - The index of the method to call. + * @param {Uint8Array} argsBuf - The buffer containing the arguments for the method call. + * @returns {StatementResult} - The result of the method call as a StatementResult object. + */ + call (jigIdx: number, methodIdx: number, argsBuf: Uint8Array): StatementResult { + const jig = this.jigAt(jigIdx) + jig.lock.assertOpen(this) + const abiClass = jig.classAbi(); + const method = abiClass.methodByIdx(methodIdx).get() + + const wasm = jig.ref.container; + const args = this.translateAndLowerArgs(wasm, method.args, argsBuf); + + this.marKJigAsAffected(jig) + const res = this.performMethodCall(jig, method, args) + + let stmt: StatementResult = res.map(ref => { + return new ValueStatementResult(this.statements.length, ref.ty, ref.ptr, ref.container) + }).orElse(() => new EmptyStatementResult(this.statements.length)) + + this.statements.push(stmt) + return stmt + } + + /** + * Executes an `EXEC` operation. It takes a function exported at `fnIndex` from + * the package provided in the statemnt number `wasmIdx` using the arguments provided + * by parameter. + * + * @param {number} wasmIdx - The index of the WebAssembly module in the `statements` array. + * @param {number} fnIdx - The index of the function to execute within the WebAssembly module. + * @param {Uint8Array} argsBuf - The arguments to pass to the function encoded in Aldea Format. + * @returns {StatementResult} - The result of executing the function. + */ exec (wasmIdx: number, fnIdx: number, argsBuf: Uint8Array): StatementResult { const wasm = this.statements[wasmIdx].asContainer() const fn = wasm.abi.exportedByIdx(fnIdx).get().toAbiFunction() @@ -289,18 +243,171 @@ class TxExecution { return stmt } + /** + * Executes a `FUND` operation. Funds using a coin at the specified index. It uses the entire balance + * and freezes the coin. + * + * @param {number} coinIdx - The index of the coin to be funded. + * + * @returns {StatementResult} - Empty statement. + * + * @throws {ExecutionError} - If the statement at the specified idx is not a coin. + */ + fund (coinIdx: number): StatementResult { + const coinJig = this.jigAt(coinIdx) + if (!coinJig.classPtr().equals(COIN_CLS_PTR)) { + throw new ExecutionError(`Not a coin: ${coinJig.origin}`) + } + + coinJig.lock.assertOpen(this) + + const amount = coinJig.getPropValue('amount').ptr + this.fundAmount += amount.toUInt() + coinJig.changeLock(new FrozenLock()) + const stmt = new EmptyStatementResult(this.statements.length) + this.statements.push(stmt) + this.marKJigAsAffected(coinJig) + return stmt + } + + /** + * Performs a `LOCK` operation. Locks the jig at the specified index using the given address. + * + * @param {number} jigIndex - The index of the jig to lock. + * @param {Address} address - The address to lock the jig to. + * @return {StatementResult} - Empty statement result. + */ + lockJig (jigIndex: number, address: Address): StatementResult { + const jigRef = this.jigAt(jigIndex) + + jigRef.lock.assertOpen(this) + this.marKJigAsAffected(jigRef) + jigRef.changeLock(new AddressLock(address)) + const ret = new EmptyStatementResult(this.statements.length); + this.statements.push(ret) + + return ret + } + + /** + * Performs a `DEPLOY` operation. Compiles the given sources and deploys them. This method + * should be awaited before executing another instruction + * + * @param {string[]} entryPoint - The entry point for the package. + * @param {Map} sources - A map of source files, where the key is the file name and the value is the source code. + * @return {Promise} - Resolves to a statement result containing the deployed package. + */ + async deploy (entryPoint: string[], sources: Map): Promise { + this.measurements.deploys.add(this.opts.deployHydroCost) + const pkgData = await this.execContext.compile(entryPoint, sources) + this.deployments.push(pkgData) + const wasm = new WasmContainer(pkgData.mod, pkgData.abi, pkgData.id) + wasm.setExecution(this) + this.wasms.set(base16.encode(wasm.hash), wasm) + const ret = new WasmStatementResult(this.statements.length, wasm) + this.statements.push(ret) + return ret + } + + /** + * Executes a `SIGN` operation. Signature verification happens in a different stage, + * so this method just keeps track of the gas usage (hydros) + * + * @param {Uint8Array} _sig - The signature to be generated. + * @param {Uint8Array} _pubKey - The public key to be used for signing. + * @return {StatementResult} - The statement result object representing the signature generation. + */ sign (_sig: Uint8Array, _pubKey: Uint8Array): StatementResult { const stmt = new EmptyStatementResult(this.statements.length); this.statements.push(stmt) return stmt } + /** + * Executes a `SIGNTO` operation. Signature verification happens in a different stage, + * so this method just keeps track of the gas usage (hydros) + * + * @param {Uint8Array} _sig - The signature. + * @param {Uint8Array} _pubKey - The public key to verify. + * @return {StatementResult} - Empty statement result. + */ signTo (_sig: Uint8Array, _pubKey: Uint8Array): StatementResult { const stmt = new EmptyStatementResult(this.statements.length); this.statements.push(stmt) return stmt } + /** + * Finishes the execution of the transaction. Returns a summary of the execution + * with the final state of the jigs, the fees paid and the hydros consumed. + * + * @return {ExecutionResult} The result of the execution. + */ + finalize (): ExecutionResult { + const result = new ExecutionResult(base16.encode(this.execContext.txHash())) + if (this.fundAmount < MIN_FUND_AMOUNT) { + throw new ExecutionError(`Not enough funding. Provided: ${this.fundAmount}. Needed: ${MIN_FUND_AMOUNT}`) + } + this.jigs.forEach(jigRef => { + if (jigRef.lock.isOpen()) { + throw new PermissionError(`Finishing tx with unlocked jig (${jigRef.className()}): ${jigRef.origin}`) + } + }) + + this.affectedJigs.forEach((jigRef, index) => { + const origin = jigRef.origin + const location = new Pointer(this.execContext.txHash(), index) + const jigProps = jigRef.extractProps() + const jigState = new Output( + origin, + location, + jigRef.classPtr(), + jigRef.lock.coreLock(), + jigProps, + jigRef.ref.container.abi.abi + ) + result.addOutput(jigState) + + if (!jigRef.isNew) { + result.addSpend(this.execContext.inputByOrigin(jigRef.origin)) + } + }) + + this.jigs.forEach(jig => { + if (!this.affectedJigs.includes(jig)) { + result.addRead(this.execContext.inputByOrigin(jig.origin)) + } + }) + + this.deployments.forEach(pkgData => { + result.addDeploy(new PackageDeploy( + pkgData.sources, + pkgData.entries, + pkgData.wasmBin, + pkgData.abi, + pkgData.docs + )) + }) + + for (const wasm of this.wasms.values()) { + wasm.clearExecution() + } + this.wasms = new Map() + this.jigs = [] + this.statements = [] + result.setHydrosUsed(this.measurements.clear()) + result.finish() + return result + } + + + /** + * Hydrates a jig from an output. If the jig is already hydrated it returns the existing one. + * + * @private + * @param {Output} output - The output to hydrate. + * @returns {JigRef} - The hydrated JigRef object. + */ private hydrate (output: Output): JigRef { const existing = this.jigs.find(j => j.origin.equals(output.origin)) if (existing) return existing @@ -327,15 +434,38 @@ class TxExecution { return newJigRef } + /** + * Resolves interpolated indexes in Argenguments and lower the data + * into the given container. + * + * @param {WasmContainer} wasm - The WebAssembly container. + * @param {AbiArg[]} args - The arguments to translate and lower. + * @param {Uint8Array} rawArgs - The raw arguments. + * @private + * + * @return {WasmWord[]} - The translated and lowered arguments as an array of WasmWord objects. + */ private translateAndLowerArgs (wasm: WasmContainer, args: AbiArg[], rawArgs: Uint8Array): WasmWord[] { - const fixer = new ArgsTranslator(this, wasm.abi) - const argsBuf = fixer.fix(rawArgs, args) + const fixer = new ArgumentsPreProcessor(this, wasm.abi) + const argsBuf = fixer.solveReferences(rawArgs, args) return this.lowerArgs(wasm, args, argsBuf) } - private lowerArgs(wasm: WasmContainer, args: AbiArg[], argsBuf: Uint8Array) { + /** + * Lower the arguments into the given container. This method + * expects that the arguments have no indexes to be resolved. + * + * @param {WasmContainer} wasm - The WebAssembly container. + * @param {AbiArg[]} args - The arguments to be lowered. + * @param {Uint8Array} argsBuf - The buffer for storing the arguments. + * + * @returns {Array} - The lowered arguments. + * + * @private + */ + private lowerArgs (wasm: WasmContainer, args: AbiArg[], argsBuf: Uint8Array) { const reader = new BufReader(argsBuf) return args.map((arg) => { @@ -343,6 +473,17 @@ class TxExecution { }) } + /** + * Performs a method call on a JigRef object. + * + * @param {JigRef} jig - The JigRef object to perform the method call on. + * @param {AbiMethod} method - The AbiMethod object representing the method to call. + * @param {WasmWord[]} loweredArgs - An array of arguments to pass to the method. + * @returns {Option} The result of the method call, wrapped in an Option container. + * If the method call is successful, it returns a ContainerRef object, + * otherwise it returns None. + * @private + */ private performMethodCall (jig: JigRef, method: AbiMethod, loweredArgs: WasmWord[]): Option { const wasm = jig.ref.container const abiClass = jig.classAbi() @@ -358,10 +499,21 @@ class TxExecution { }) } - createNextOrigin () { + /** + * Creates the origin for the next jig created in the current tx. + * + * @returns {Pointer} The new Pointer for the next instance. + */ + private createNextOrigin () { return new Pointer(this.execContext.txHash(), this.affectedJigs.length) } + /** + * Returns the statement at the given index. + * + * @param {number} index - The index of the statement. + * @returns {StatementResult} - The statement at the given index. + */ stmtAt (index: number): StatementResult { const result = this.statements[index] if (!result) { @@ -370,15 +522,16 @@ class TxExecution { return result } - private lockJigToUser (jigRef: JigRef, address: Address): StatementResult { - jigRef.lock.assertOpen(this) - this.marKJigAsAffected(jigRef) - jigRef.changeLock(new AddressLock(address)) - const ret = new EmptyStatementResult(this.statements.length); - this.statements.push(ret) - return ret - } - + /** + * Retrieves the JigRef at the specified index. + * + * @param {number} jigIndex - The index of the JigRef to retrieve. + * @private + * + * @return {JigRef} The JigRef at the specified index. + * @throws {ExecutionError} If the index is not a valid JigRef. + * @throws {InvariantBroken} If the lowered JigRef is not in the list of jigs. + */ private jigAt (jigIndex: number): JigRef { const ref = this.stmtAt(jigIndex).asValue() ref.container.abi.exportedByName(ref.ty.name) @@ -389,26 +542,18 @@ class TxExecution { const lifted = Pointer.fromBytes(ref.lift()) return Option.fromNullable(this.jigs.find(j => j.origin.equals(lifted))) - .expect(new IvariantBroken('Lowered jig is not in jig list')) + .expect(new InvariantBroken('Lowered jig is not in jig list')) } - lockJig (jigIndex: number, address: Address): StatementResult { - const jigRef = this.jigAt(jigIndex) - return this.lockJigToUser(jigRef, address) - } - - async deploy (entryPoint: string[], sources: Map): Promise { - this.measurements.deploys.add(this.opts.deployHydroCost) - const pkgData = await this.execContext.compile(entryPoint, sources) - this.deployments.push(pkgData) - const wasm = new WasmContainer(pkgData.mod, pkgData.abi, pkgData.id) - wasm.setExecution(this) - this.wasms.set(base16.encode(wasm.hash), wasm) - const ret = new WasmStatementResult(this.statements.length, wasm) - this.statements.push(ret) - return ret - } + /** + * If a container with the id `modId` was provided by context, it returns it. + * Otherwise, it fails the execution. + * + * @param {string} modId - The module ID of the WasmContainer to assert. + * @returns {WasmContainer} - The asserted or newly created WasmContainer. + * @private + */ private assertContainer (modId: string): WasmContainer { const existing = this.wasms.get(modId) if (existing) { @@ -422,22 +567,38 @@ class TxExecution { return container } - import (modId: Uint8Array): StatementResult { - const instance = this.assertContainer(base16.encode(modId)) - const ret = new WasmStatementResult(this.statements.length, instance); - this.statements.push(ret) - return ret - } + /** + * Checks if the given address is part of the signers of the tx. + * The signers are not take from the `SIGN` and `SIGNTO` operations. Instead + * they are provided by context. + * + * @param {Address} addr - The address to check. + * @return {boolean} - True if the given address is signed by any signer, false otherwise. + */ signedBy (addr: Address): boolean { return this.execContext.signers() .some(s => s.toAddress().equals(addr)) } + /** + * Returns how many opcodes where executed. It's the equivalent of the + * "instruction pointer" in a regular machine. + * + * @return {number} The length of the statements array. + */ execLength () { return this.statements.length } + /** + * Marks the given Jig as affected. Affected jigs are the ones that + * get into the outpus at the end of the transaction. + * + * @param {JigRef} jig - The Jig object to mark as affected. + * + * @private + */ private marKJigAsAffected (jig: JigRef): void { const exists = this.affectedJigs.find(affectedJig => affectedJig.origin.equals(jig.origin)) if (!exists) { @@ -445,6 +606,16 @@ class TxExecution { } } + /** + * Retrieves the JigData for a given Pointer. If the jig data is not provided + * by the context it fails. + * + * This method is used to lower data into wasm memory. + * + * @param {Pointer} p - The Pointer for which to retrieve the JigData. + * @returns {Option} - An Option container that may contain the JigData for the given Pointer. + * If the JigData is found, it returns a JigData object, otherwise it returns None. + */ getJigData (p: Pointer): Option { const existing = this.jigs.find(j => j.origin.equals(p)) if (existing) { @@ -467,6 +638,14 @@ class TxExecution { }) } + /** + * If a jig with the given origin is found, it returns it. Otherwise, it + * tries to hydrate it and return it. If the jig is not present it fails. + * + * @param {Pointer} origin - The origin to check for the jig. + * @private + * @returns {Option} - The found jig or the result of hydrating the output from the execution context. + */ private assertJig (origin: Pointer) { return Option.fromNullable( this.jigs.find(j => j.origin.equals(origin)) @@ -476,6 +655,17 @@ class TxExecution { }) } + /** + * Lifts the arguments from the given container and returns them packed in a Uint8Array. + * + * @param {WasmContainer} from - The container from which to lift the arguments. + * @param {WasmWord} ptr - The pointer to the arguments. + * @param {AbiArg[]} argDef - The definition of the arguments. + * + * @returns {Uint8Array} - The lifted arguments. + * + * @private + */ private liftArgs (from: WasmContainer, ptr: WasmWord, argDef: AbiArg[]): Uint8Array { const w = new BufWriter() @@ -490,6 +680,16 @@ class TxExecution { return w.data } + + + /** + * Returns the pointer at the top of the permission stack. + * + * @param {number} n - The number of elements to go back in the stack. + * @returns {Option} - The pointer at the desired position. + * + * @private + */ stackFromTop (n: number): Option { return Option.fromNullable(this.stack[this.stack.length - n]) } @@ -501,12 +701,26 @@ class TxExecution { // Metering callbacks + /** + * Callback used to keep track of the amount of data being + * moved in an out of wasm memory. + * + * @param {number} size - The size of the data that is moved. + * + * @return {void} + */ onDataMoved (size: number) { this.measurements.movedData.add(BigInt(size)) } // Assemblyscrypt callbacks + /** + * Callback used mainly to provide Origins to new jigs being created. + * + * @param {WasmContainer} from - Container where event was produced. + * @return {WasmArg} - Pointer to new jig data. + */ vmJigInit (from: WasmContainer): WasmArg { this.measurements.newJigs.inc() const nextOrigin = this.nextOrigin.get() @@ -519,6 +733,19 @@ class TxExecution { return jigInitParams.lowerInto(from).toWasmArg(AbiType.u32()) } + /** + * Links a Jig to a runtime ID (rtId) in a WebAssembly container. + * This callback is used to associate an already initialized Jig with a specific class id. + * The new jig goes to the outputs of the transaction. + * The class pointer of the class of the new jig gets lowered into the container and that's + * the final result. + * + * @param {WasmContainer} from - The WebAssembly container where the class is defined. + * @param {WasmWord} jigPtr - The pointer to the Jig to be linked. + * @param {number} rtId - The runtime ID of the class in the WebAssembly module. + * @returns {WasmArg} - A serialized pointer to the class in the WebAssembly module. + * @throws {Error} If the runtime ID or class name is not found in the WebAssembly module. + */ vmJigLink (from: WasmContainer, jigPtr: WasmWord, rtId: number): WasmArg { const nextOrigin = this.nextOrigin.get() let rtIdNode = from.abi.rtIdById(rtId) @@ -546,10 +773,20 @@ class TxExecution { .low .lower( serializePointer(new Pointer(from.hash, abiClass.idx)), - AbiType.fromName('ArrayBuffer') + AbiType.buffer() ).toWasmArg(AbiType.u32()) } + /** + * Callback executed when a jig calls a method over a jig other than itself. + * The arguments are send to the target jig, and the response is routed back to the caller. + * + * @param {WasmContainer} from - The source WasmContainer. + * @param {WasmWord} targetPtr - The pointer to the target object in memory. + * @param {WasmWord} methodNamePtr - The pointer to the method name in memory. + * @param {WasmWord} argsPtr - The pointer to the arguments in memory. + * @returns {WasmArg} - Pointer to the lowered result in the source container. + */ vmCallMethod (from: WasmContainer, targetPtr: WasmWord, methodNamePtr: WasmWord, argsPtr: WasmWord): WasmArg { const targetOrigin = Pointer.fromBytes(from.liftBuf(targetPtr)) const methodName = from.liftString(methodNamePtr) @@ -562,10 +799,6 @@ class TxExecution { // Move args const argBuf = this.liftArgs(from, argsPtr, method.args) const loweredArgs = this.lowerArgs(jig.ref.container, method.args, argBuf) - // const argsReader = new BufReader(argBuf) - // const loweredArgs = method.args.map(arg => { - // return jig.ref.container.low.lowerFromReader(argsReader, arg.type) - // }) const methodRes = this.performMethodCall(jig, method, loweredArgs) this.marKJigAsAffected(jig) @@ -576,6 +809,16 @@ class TxExecution { }).orElse(() => WasmWord.fromNumber(0)).toWasmArg(method.rtype) } + /** + * Callback executed when a jig access to a property (fields) of a jig other + * than itself. The data is routed from the target container to the source + * container. The result is a pointer to the data. + * + * @param {WasmContainer} from - The object from which to retrieve the property. + * @param {WasmWord} originPtr - The pointer to the origin of the object. + * @param {WasmWord} propNamePtr - The pointer to the property name. + * @return {WasmArg} - The value of the retrieved property. + */ vmGetProp (from: WasmContainer, originPtr: WasmWord, propNamePtr: WasmWord): WasmArg { const targetOrigin = Pointer.fromBytes(from.liftBuf(originPtr)); const propName = from.liftString(propNamePtr) @@ -587,6 +830,19 @@ class TxExecution { return word.toWasmArg(propTarget.ty) } + + /** + * Callback used when a jig tries to lock another jig. + * The lock type can be one of the following: ADDRESS, JIG, FROZEN, PUBLIC, NONE. + * The data should follow the locktype, otherwise it's an error. + * The target jig is marked as affected. + * + * @param {WasmContainer} from - The WebAssembly container where the Jig is defined. + * @param {WasmWord} targetOriginPtr - The pointer to the origin of the Jig to be locked. + * @param {LockType} lockType - The type of the lock to be applied to the Jig. + * @param {WasmWord} argsPtr - The pointer to the arguments to be used for creating the lock. + * @throws {Error} If an unknown lock type is provided. + */ vmJigLock (from: WasmContainer, targetOriginPtr: WasmWord, lockType: LockType, argsPtr: WasmWord): void { const origin = Pointer.fromBytes(from.liftBuf(targetOriginPtr)) @@ -616,6 +872,17 @@ class TxExecution { this.marKJigAsAffected(jig) } + /** + * Callback executeed when a jig uses a constructor defined on the same package. + * A new instance of the class is creted and the result is a pointer to the instance + * already initialized and ready. + * + * @param {WasmContainer} from - The container when the event was raised. + * @param {WasmWord} clsNamePtr - The memory pointer to the class name of the Jig to be initialized. + * @param {WasmWord} argsPtr - The memory pointer to the arguments for the Jig constructor. + * @returns {WasmWord} - Pointer to newly created jig. + * @throws {InvariantBroken} - If after calling the constructor the jig is not there. + */ vmConstructorLocal (from: WasmContainer, clsNamePtr: WasmWord, argsPtr: WasmWord): WasmWord { const clsName = from.liftString(clsNamePtr) @@ -634,7 +901,7 @@ class TxExecution { const createdJig = this.jigs.find(ref => ref.origin.equals(nextOrigin)) if (!createdJig) { - throw new ExecutionError(`[line=${this.execLength()}]Jig was not created`) + throw new InvariantBroken(`[line=${this.execLength()}]Jig was not created`) } const initParams = new JigInitParams( @@ -647,6 +914,18 @@ class TxExecution { return initParams.lowerInto(from) } + /** + * Callback executed when a jig uses an exported function from another package. + * Calling an exported function does not affect the permission stack. + * The result of the pointer is routed to the origin container. If it's a jig a proxy + * is going to be lowered in the source container. + * + * @param {WasmContainer} from - The WasmContainer instance. + * @param {WasmWord} pkgIdPtr - Pointer to the package ID string. + * @param {WasmWord} fnNamePtr - Pointer to the function name string. + * @param {WasmWord} argsBufPtr - Pointer to the arguments buffer. + * @return {WasmArg} - The result of the function call. + */ vmCallFunction (from: WasmContainer, pkgIdPtr: WasmWord, fnNamePtr: WasmWord, argsBufPtr: WasmWord): WasmArg { const pkgId = from.liftString(pkgIdPtr) const fnName = from.liftString(fnNamePtr) @@ -660,6 +939,18 @@ class TxExecution { return res.orDefault(WasmWord.null()).toWasmArg(fn.rtype); } + /** + * Callback executed when a jig uses a constructor defined in another package. + * A new instance of the class is created and the result is a pointer to the instance + * already initialized and ready. + * + * @param {WasmContainer} from - The container from which to create the instance. + * @param {WasmWord} pkgIdStrPtr - The pointer to the package ID string. + * @param {WasmWord} namePtr - The pointer to the class name. + * @param {WasmWord} argBufPtr - The pointer to the argument buffer. + * @return {WasmArg} - Pointer to a proxy to the newly created jig. + * @throws {InvariantBroken} - If after the constructor finishes the jig is not there. + */ vmConstructorRemote (from: WasmContainer, pkgIdStrPtr: WasmWord, namePtr: WasmWord, argBufPtr: WasmWord): WasmArg { const pkgId = from.liftString(pkgIdStrPtr) const clsName = from.liftString(namePtr) @@ -683,7 +974,7 @@ class TxExecution { const jig = this.jigs.find(j => j.origin.equals(nextOrigin)) if (!jig) { - throw new Error('jig should exist') + throw new InvariantBroken(`[line=${this.execLength()}] created jig was not found`) } const initParams = new JigInitParams( @@ -696,6 +987,16 @@ class TxExecution { return initParams.lowerInto(from).toWasmArg(AbiType.u32()); } + /** + * Callback executed when a jig checks the type of the caller against a locally + * defined type. + * + * @param {WasmContainer} from - The caller's WasmContainer object. + * @param {number} rtIdToCheck - The runtime id to check against. + * @param {boolean} exact - Determines if the check should be exact or should check for parent classes. + * + * @return {boolean} - Returns true if the caller type matches the specified runtime id, otherwise returns false. + */ vmCallerTypeCheck (from: WasmContainer, rtIdToCheck: number, exact: boolean): boolean { const maybeOrigin = this.stackFromTop(2) if (maybeOrigin.isAbsent()) { @@ -724,10 +1025,21 @@ class TxExecution { return true } - vmCallerOutputCheck () { + /** + * Checks if the caller of the current method is a jig (has an output) or not. + * + * @return {boolean} Returns true if the caller has a valid output, otherwise false. + */ + vmCallerOutputCheck (): boolean { return this.stackFromTop(2).isPresent(); } + /** + * Callback executed when a jig tries to access the output of the caller. + * Throws an error if the caller is not a jig. + * + * @return {WasmWord} - Pointer to the caller's output. + */ vmCallerOutput (from: WasmContainer): WasmWord { const callerOrigin = this.stackFromTop(2).get(); const callerJig = this.assertJig(callerOrigin) @@ -740,8 +1052,18 @@ class TxExecution { return from.low.lower(buf.data, new AbiType(outputTypeNode)); } + /** + * Returns data from the output of the caller. The options are: origin, location, class. + * If there is not caller output it throws an error. + * + * @param {WasmContainer} from - The WasmContainer object. + * @param {WasmWord} keyPtr - The key pointer. + * @returns {WasmWord} - The output value. + * @throws {InvariantBroken} - Throws an error if the key is unknown. + * @throws {ExecutionError} - If there is no caller jig. + */ vmCallerOutputVal (from: WasmContainer, keyPtr: WasmWord): WasmWord { - const callerOrigin = this.stackFromTop(2).get(); + const callerOrigin = this.stackFromTop(2).expect(new ExecutionError('No caller')); const key = from.liftString(keyPtr) const callerJig = this.assertJig(callerOrigin) @@ -758,12 +1080,24 @@ class TxExecution { buf.writeBytes(callerJig.classPtr().toBytes()) break default: - throw new Error(`unknown vmCallerOutputVal key: ${key}`) + throw new InvariantBroken(`unknown vmCallerOutputVal key: ${key}`) } - return from.low.lower(buf.data, AbiType.fromName('ArrayBuffer')) + return from.low.lower(buf.data, AbiType.buffer()) } + /** + * Callback executed when a jig checks permissions over another jig. + * The src jig is taken from the permission stack and the target is specified + * by parameter. The check can be one of the following: LOCK (change lock), + * CALL (receive method calls from the src jig). + * + * @param {WasmContainer} from - The source container. + * @param {WasmWord} targetOriginPtr - The pointer to the target origin. + * @param {AuthCheck} check - The type of authorization check to perform. + * @returns {boolean} - True if the operation is authorized, false otherwise. + * @throws {Error} - If the provided auth check is unknown. + */ vmJigAuthCheck (from: WasmContainer, targetOriginPtr: WasmWord, check: AuthCheck): boolean { const origin = Pointer.fromBytes(from.liftBuf(targetOriginPtr)) const jig = this.assertJig(origin) @@ -777,6 +1111,13 @@ class TxExecution { } } + /** + * Takes the gas used by the last webassembly execution chunk and adds + * it to the total. This callback is called after every execution chunk. + * + * @param {bigint} gasUsed - The amount of gas used during the execution. + * @throws {ExecutionError} - If the gas used is greater than the gas limit. + */ vmMeter (gasUsed: bigint) { this.measurements.wasmExecuted.add(gasUsed) } diff --git a/packages/vm/src/vm.ts b/packages/vm/src/vm.ts index b83251c8..e7092010 100644 --- a/packages/vm/src/vm.ts +++ b/packages/vm/src/vm.ts @@ -1,7 +1,7 @@ -import {PkgData, Storage} from "./storage.js"; +import {MemStorage} from "./storage/mem-storage.js"; import {CompilerResult, PackageParser, writeDependency} from '@aldea/compiler' -import {abiFromBin, Address, BCS, OpCode, Output, Pointer, Tx, util} from "@aldea/core"; -import {calculatePackageId} from "./calculate-package-id.js"; +import {abiFromBin, Address, base16, BCS, OpCode, Output, Pointer, Tx, util} from "@aldea/core"; +import {calculatePackageHash} from "./calculate-package-hash.js"; import {Buffer} from "buffer"; import {data as wasm} from './builtins/coin.wasm.js' import {data as rawAbi} from './builtins/coin.abi.bin.js' @@ -9,38 +9,63 @@ import {data as rawDocs} from './builtins/coin.docs.json.js' import {data as rawSource} from './builtins/coin.source.js' import {AddressLock} from "./locks/address-lock.js"; import {ExecutionResult} from "./execution-result.js"; -import {StorageTxContext} from "./tx-context/storage-tx-context.js"; +import {StorageTxContext} from "./exec-context/storage-tx-context.js"; import {TxExecution} from "./tx-execution.js"; import { CallInstruction, + DeployInstruction, + ExecInstruction, + FundInstruction, ImportInstruction, LoadByOriginInstruction, LoadInstruction, + LockInstruction, NewInstruction, - ExecInstruction, FundInstruction, LockInstruction, DeployInstruction, SignInstruction, SignToInstruction + SignInstruction, + SignToInstruction } from "@aldea/core/instructions"; -import {ExecOpts} from "./export-opts.js"; +import {ExecOpts} from "./exec-opts.js"; +import {COIN_PKG_ID} from "./well-known-abi-nodes.js"; +import {ExecutionError} from "./errors.js"; +import {PkgData} from "./storage/pkg-data.js"; -// Magic Coin Pkg ID -const COIN_PKG_ID = new Uint8Array([ - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -]) +/** + * Type for compiling functions. This type follows + * the type of the basic compile function for @aldea/compiler + */ export type CompileFn = (entry: string[], src: Map, deps: Map) => Promise; + +/** + * A class representing an Aldea virtual machine (VM). + * + * The VM takes and saves data to the `Storage`. + */ export class VM { - private readonly storage: Storage; + private readonly storage: MemStorage; private readonly compile: CompileFn; - constructor (storage: Storage, compile: CompileFn) { + /** + * Creates a new instance of the constructor. + * + * @param {MemStorage} storage - The storage object for retrieving storing data. + * @param {CompileFn} compile - The compile function to manaage deploys. + */ + constructor (storage: MemStorage, compile: CompileFn) { this.storage = storage this.compile = compile this.addPreCompiled(wasm, rawSource, rawAbi, rawDocs, COIN_PKG_ID) } + /** + * Executes an Aldea transaction. If the execution is correct it saves the result into the + * storage. + * + * @param {Tx} tx - Aldea transaction to execute. + * @return {Promise} - The result of the execution. + * @throws {ExecutionError} - When an unknown opcode is encountered. + */ async execTx(tx: Tx): Promise { const context = new StorageTxContext(tx.hash, tx.signers(), this.storage, this) const currentExec = new TxExecution(context, ExecOpts.default()) @@ -93,25 +118,25 @@ export class VM { currentExec.signTo(signTo.sig, signTo.pubkey) break default: - throw new Error(`unknown opcode: ${inst.opcode}`) + throw new ExecutionError(`unknown opcode: ${inst.opcode}`) } } const result = currentExec.finalize() - this.storage.persistTx(tx) - this.storage.persistExecResult(result) + await this.storage.persistTx(tx) + await this.storage.persistExecResult(result) return result } - // async execTxFromInputs(exTx: ExtendedTx) { - // const context = new ExTxExecContext(exTx, this.clock, this.pkgs, this) - // const currentExecution = new TxExecution(context) - // const result = await currentExecution.run() - // this.storage.persist(result) - // return result - // } - + /** + * Compiles the given sources and returns package data. At this point the single line imports are resolved. + * + * @param entries - An array of entry points for the package. + * @param sources - A map of source code files, where the key is the file name and the value is the source code. + * + * @returns A Promise that resolves to a PkgData object containing all the data of the package. + */ async compileSources (entries: string[], sources: Map): Promise { - const id = calculatePackageId(entries, sources) + const id = calculatePackageHash(entries, sources) const pkg = await PackageParser.create(entries, { getSrc: (src) => sources.get(src), @@ -134,32 +159,55 @@ export class VM { ) } + /** + * Adds a pre-compiled module to the storage. This is useful to generate controlled environments or test + * very specific use cases, but it's not something that a real production node can do. + * + * @param {Uint8Array} wasmBin - The binary representation of the WebAssembly module. + * @param {string} sourceStr - The source code of the module in string format. + * @param {Uint8Array} abiBin - The binary representation of the module's ABI (Application Binary Interface). + * @param {Uint8Array} docs - The documentation of the module in binary format. + * @param {Uint8Array|null} [defaultId=null] - The default package ID. If not provided, it will be calculated based on the entries and sources. + * + * @returns {Uint8Array} - The package ID of the added module. + */ addPreCompiled (wasmBin: Uint8Array, sourceStr: string, abiBin: Uint8Array, docs: Uint8Array, defaultId: Uint8Array | null = null): Uint8Array { const sources = new Map() sources.set('index.ts',sourceStr.toString()) const entries = ['index.ts']; - const id = defaultId + const hash = defaultId ? defaultId - : calculatePackageId(entries, sources) - if (this.storage.hasModule(id)) { - return id + : calculatePackageHash(entries, sources) + const id = base16.encode(hash) + if (this.storage.getPkg(id).isPresent()) { + return hash } const abi = abiFromBin(abiBin) - this.storage.addPackage(id, new PkgData( + this.storage.addPackage(hash, new PkgData( abi, docs, entries, - id, + hash, new WebAssembly.Module(wasmBin), sources, wasmBin )) - return id + return hash } - mint (address: Address, amount: number = 1e6, locBuf?: Uint8Array): Output { + /** + * Mint 1 coin with the given amount and locked to the given address. This method + * goes outside consensus and is meant to be used in development environments. + * + * @param {Address} address - The address to mint the output for. + * @param {number} [amount=1e6] - The amount of the output. Defaults to 1e6. + * @param {Uint8Array} [locBuf] - Optional location buffer to use. If not provided, a random location buffer will be generated. + * + * @return {Output} The minted output. + */ + async mint (address: Address, amount: number = 1e6, locBuf?: Uint8Array): Promise { const location = locBuf ? new Pointer(locBuf, 0) : new Pointer(util.randomBytes(32), 0); @@ -171,7 +219,7 @@ export class VM { new AddressLock(address).coreLock(), bcs.encode('u64', amount) ) - this.storage.addUtxo(minted) + await this.storage.addUtxo(minted) return minted } } diff --git a/packages/vm/src/wasm-container.ts b/packages/vm/src/wasm-container.ts index 29cb0c94..a285d825 100644 --- a/packages/vm/src/wasm-container.ts +++ b/packages/vm/src/wasm-container.ts @@ -15,18 +15,40 @@ export enum AuthCheck { LOCK } +/** + * Represents a WebAssembly live WebAssembly module. It is instantiated from a Package (the result + * of compile sources). There is access to it's memory and methods exposed. + * + * A WasmContainer needs an associated execution to be able to run. + */ export class WasmContainer { + // Package ID hash: Uint8Array; + // WebAssembly memory. The low level representation memory: WebAssembly.Memory; + // The current execution associated with this container private _currentExec: Option; - + // The low level WebAssembly module private module: WebAssembly.Module; + // The low level WebAssembly instance private instance: WebAssembly.Instance; + // Aldea ABI for the code of the container abi: AbiAccess; + // Memory is accessed from the outside through this proxy private _mem: MemoryProxy; + // Object to lift values from memory lifter: ValueLifter + // Object to lower values into memory low: LowerValue + /** + * Constructs a new WasmContainer. It sets all the imports to functions that delegate + * to the current execution. + * + * @param {WebAssembly.Module} module - The WebAssembly module. + * @param {Abi} abi - The ABI for the WebAssembly module. + * @param {Uint8Array} id - The ID of the WebAssembly module. + */ constructor (module: WebAssembly.Module, abi: Abi, id: Uint8Array) { this.hash = id this.abi = new AbiAccess(abi) @@ -37,6 +59,7 @@ export class WasmContainer { this.lifter = new ValueLifter(this) this.low = new LowerValue(this, (p) => this._currentExec.get().getJigData(p)) + // Delegate all imports to the current execution const imports: any = { env: { memory: wasmMemory, @@ -121,7 +144,7 @@ export class WasmContainer { return this._currentExec.ifPresent(vm => vm.vmMeter(gasUsed)) }, debug_str: (strPtr: number): void => { - const msg = this.lifter.lift(WasmWord.fromNumber(strPtr), AbiType.fromName('string')) + const msg = this.lifter.lift(WasmWord.fromNumber(strPtr), AbiType.string()) const buf = Buffer.from(new BufReader(msg).readBytes()) console.log(`debug [pkg=${this.id.slice(0, 6)}...]: ${buf.toString()}`) } @@ -134,40 +157,77 @@ export class WasmContainer { this.memory = wasmMemory } + /** + * Callback to keep track of data movement. Delegates in current execution. + * @param {number} size - The size of the data moved. + */ private onDataMoved(size: number): void { this._currentExec.ifPresent(exec => exec.onDataMoved(size)) } + /** + * Returns the ID of the container as a string. + * + * @returns {string} The ID of the container as a string. + */ get id (): string { return base16.encode(this.hash) } + /** + * Gets the memory proxy to lower and lift data. + * @returns {MemoryProxy} The memory proxy of the WebAssembly container. + */ get mem (): MemoryProxy { return this._mem } - get currentExec (): Option { - return this._currentExec - } - + /** + * Sets a new current execution for this container. + * @param {TxExecution} tx - The current execution. + */ setExecution (tx: TxExecution) { this._currentExec = Option.some(tx) } + /** + * Clears the current execution. + * + * @return {void} + */ clearExecution () { this._currentExec = Option.none() } + /** + * Helper to easily lift a string from container's memory. + * + * @param {WasmWord} ptr - The pointer to the string in memory. + * @return {string} - The lifted string. + */ liftString(ptr: WasmWord): string { - const buf = this.lifter.lift(ptr, AbiType.fromName('string')) + const buf = this.lifter.lift(ptr, AbiType.string()) return Buffer.from(new BufReader(buf).readBytes()).toString() } + /** + * Helper to easily lift a buffer from container's memory. + * + * @param {WasmWord} ptr - The pointer to the buffer in memory. + * @return {Uint8Array} - The lifted buffer. + */ liftBuf(ptr: WasmWord): Uint8Array { - const buf = this.lifter.lift(ptr, AbiType.fromName('ArrayBuffer')) + const buf = this.lifter.lift(ptr, AbiType.buffer()) return new BufReader(buf).readBytes() } + + /** + * Allocates memory in the WebAssembly memory for an object. + * @param {number} size - The size of the memory to allocate. + * @param {number} rtid - The runtime ID for the allocated object. + * @returns {WasmWord} The pointer to the allocated memory. + */ malloc (size: number, rtid: number): WasmWord { const __new = this.instance.exports.__new; if (!(__new instanceof Function)) { @@ -177,6 +237,13 @@ export class WasmContainer { return WasmWord.fromNumber(ptrNumber) } + /** + * Calls an exported function from the container. + * @param {string} fnName - The name of the function to call. + * @param {WasmWord[]} wasmWords - The arguments for the function call. + * @param {AbiType[]} abiTypes - The ABI types of the arguments. + * @returns {Option} The result of the function call. + */ callFn (fnName: string, wasmWords: WasmWord[], abiTypes: AbiType[]): Option { const fn = this.instance.exports[fnName]; if (!(fn instanceof Function)) { @@ -187,6 +254,10 @@ export class WasmContainer { return Option.fromNullable(ret).map(r => WasmWord.fromNumeric(r)) } + /** + * BCS instance initialized with the current ABI. + * @returns {BCS} The BCS for the WebAssembly container. + */ bcs (): BCS { return new BCS(this.abi.abi) } diff --git a/packages/vm/src/wasm-word.ts b/packages/vm/src/wasm-word.ts index 5949eb49..0e007c0c 100644 --- a/packages/vm/src/wasm-word.ts +++ b/packages/vm/src/wasm-word.ts @@ -1,11 +1,32 @@ import {AbiType} from "./memory/abi-helpers/abi-type.js"; import {BufReader, BufWriter} from "@aldea/core"; +/** + * Represents an argument that can be passed to a WebAssembly function. + * Wasm u64 are represented by `bigint` in JavaScript. Choosing between + * `number` and `bigint` is done dynamically by the VM. + * @typedef {(number | bigint)} WasmArg + */ export type WasmArg = number | bigint +/** + * Represents a word in WebAssembly, which can be a number or a bigint. + * A word in a CPU context is the size of a register or the amount of data + * that is used in an atomic operation. + * + * This class is used as an intermediary between WASM and JS, and it has + * all kind of utility methods to move between one world and the other. + */ export class WasmWord { + /* Data is internally saved as 8 bytes */ private value: ArrayBuffer; + /** + * Constructs a WasmWord from an ArrayBuffer. + * The ArrayBuffer should not be longer than 8 bytes. + * @param {ArrayBuffer} bytes - The ArrayBuffer to construct the WasmWord from. + * @throws {Error} If the ArrayBuffer is longer than 8 bytes. + */ constructor (bytes: ArrayBuffer) { if (bytes.byteLength > 8) { throw new Error('wrong number of bytes') @@ -14,6 +35,13 @@ export class WasmWord { Buffer.from(this.value).set(Buffer.from(bytes)) } + /** + * Constructs a WasmWord from a number. The number can be a float or an integer, and the + * representation gets automatically chosen. + * + * @param {number} number - The number to construct the WasmWord from. + * @returns {WasmWord} The constructed WasmWord. + */ static fromNumber (number: number): WasmWord { const w = new BufWriter({ size: 8 }) if (Number.isInteger(number)) { @@ -24,12 +52,24 @@ export class WasmWord { return new this(w.data) } + /** + * Creates a new instance of `WasmWord` from a `bigint` value. + * + * @param {bigint} bigint - The `bigint` value to convert. + * @returns {WasmWord} - A new instance of `WasmWord`. + */ static fromBigInt (bigint: bigint): WasmWord { const w = new BufWriter({size: 8}) w.writeU64(bigint) return new this(w.data) } + /** + * Converts a numeric (`number` or `bigint`) value to a `WasmWord`. + * + * @param {WasmArg} value - The numeric value to convert. + * @return {WasmWord} - The converted WasmWord. + */ static fromNumeric (value: WasmArg): WasmWord { if (typeof value === 'bigint') { return this.fromBigInt(value) @@ -38,40 +78,90 @@ export class WasmWord { } } + /** + * Creates a new WasmWord instance from a BufReader. It needs the type + * of the value to know the size. + * + * @param {BufReader} read - The BufReader to read from. + * @param {AbiType} ty - The AbiType used to determine the size of the fixed bytes. + * @return {WasmWord} A new WasmWord instance. + */ static fromReader (read: BufReader, ty: AbiType = AbiType.fromName('u32')): WasmWord { return new WasmWord(read.readFixedBytes(ty.ownSize())) } + /** + * Converts the WasmWord to a signed integer. + * @returns {number} The integer representation of the WasmWord. + */ toInt (): number { return Buffer.from(this.value).readInt32LE() } + /** + * Converts the WasmWord to an unsigned integer. + * @returns {number} The unsigned integer representation of the WasmWord. + */ toUInt (): number { return Buffer.from(this.value).readUInt32LE() } + /** + * Converts the WasmWord to a float. + * @returns {number} The float representation of the WasmWord. + */ toFloat (): number { return new BufReader(new Uint8Array(this.value)).readF64() } + /** + * Converts the WasmWord to a bigint. + * @returns {bigint} The bigint representation of the WasmWord. + */ toBigInt (): bigint { return Buffer.from(this.value).readBigInt64LE() } + /** + * Converts the WasmWord to a boolean. + * @returns {boolean} The boolean representation of the WasmWord. Returns true if the integer representation of the WasmWord is not 0, false otherwise. + */ toBool (): boolean { return this.toInt() !== 0 } + /** + * Adds a number to the WasmWord. This can be used for pointer arithmetic. + * It interprets the content of the word as integet to make the addition. + * + * @param {number} n - The number to add to the WasmWord. + * @returns {WasmWord} A new WasmWord that is the result of the addition. + */ plus(n: number): WasmWord { let num = Buffer.from(this.value).readUInt32LE() return WasmWord.fromNumber(num + n) } + /** + * Subtracts a number from the WasmWord. It can be used for pointer arithmetic. + * It interprets the word as an integer. + * + * @param {number} n - The number to subtract from the WasmWord. + * @returns {WasmWord} A new WasmWord that is the result of the subtraction. + */ minus(n: number): WasmWord { let num = this.toInt() return WasmWord.fromNumber(num - n) } + /** + * Aligns the WasmWord to a specified size. Align basically means make it a multiple of the given size. + * This is useful for memory operations. + * If the WasmWord is already aligned to the specified size, it returns the WasmWord itself. + * Otherwise, it returns a new WasmWord which integer representation is alligned. + * @param {number} toSize - The size to align the WasmWord to the desired number. + * @returns {WasmWord} The aligned WasmWord. + */ align (toSize: number): WasmWord { const self = this.toInt() const rem = self % toSize @@ -82,41 +172,22 @@ export class WasmWord { } } + /** + * Serializes the WasmWord to a Uint8Array. It needs a type to know the size of the value. + * @param {AbiType} ty - The ABI type to determine the length of the Uint8Array. + * @returns {Uint8Array} The serialized Uint8Array. + */ serialize(ty: AbiType): Uint8Array { - // let writer: BufWriter - // switch (ty.name) { - // case 'bool': - // case 'u8': - // case 'i8': - // writer = new BufWriter({size: 1}) - // writer.writeU8(this.toNumber()) - // return writer.data - // case 'u16': - // case 'i16': - // writer = new BufWriter({size: 2}) - // writer.writeU16(this.toNumber()) - // return writer.data - // case 'u64': - // case 'i64': - // writer = new BufWriter({size: 8}) - // writer.writeU64(this.toBigInt()) - // return writer.data - // case 'f32': - // writer = new BufWriter({size: 4}) - // writer.writeF32(this.toNumber()) - // return writer.data - // case 'f64': - // writer = new BufWriter({size: 8}) - // writer.writeF64(this.toNumber()) - // return writer.data - // default: - // writer = new BufWriter({size: 4}) - // writer.writeU32(this.toNumber()) - // return writer.data - // } return new Uint8Array(this.value, 0, ty.ownSize()) } + /** + * Converts the WasmWord to a WasmArg. This is used to send the word to the WasmContainer. + * It needs a type to determine the right way to convert the WasmWord. + * + * @param {AbiType} abiType - The ABI type to determine the type of the WasmArg. + * @returns {WasmArg} The WasmArg representation of the WasmWord. + */ toWasmArg (abiType: AbiType): WasmArg { if (['u64', 'i64'].includes(abiType.name)) return this.toBigInt() if ('f32' === abiType.name) return this.toFloat() @@ -124,10 +195,19 @@ export class WasmWord { return this.toInt() } + /** + * Checks if the WasmWord is equal to another WasmWord. + * @param {WasmWord} another - The other WasmWord to compare with. + * @returns {boolean} True if the two WasmWords are equal, false otherwise. + */ equals (another: WasmWord) { return Buffer.from(this.value).equals(Buffer.from(another.value)); } + /** + * Constructs a WasmWord representing a null pointer. + * @returns {WasmWord} The null WasmWord. + */ static null () { return this.fromNumber(0) } diff --git a/packages/vm/src/memory/well-known-abi-nodes.ts b/packages/vm/src/well-known-abi-nodes.ts similarity index 89% rename from packages/vm/src/memory/well-known-abi-nodes.ts rename to packages/vm/src/well-known-abi-nodes.ts index e7fb9002..bd573733 100644 --- a/packages/vm/src/memory/well-known-abi-nodes.ts +++ b/packages/vm/src/well-known-abi-nodes.ts @@ -70,20 +70,12 @@ export const lockAbiNode: ObjectNode = { ] } -export const voidNode = emptyTn('_void') - export const coinNode: ImportCode = { name: 'Coin', pkg: new Array(32).fill('0').join(''), kind: CodeKind.PROXY_CLASS } -export const jigNode: ImportCode = { - name: 'Jig', - pkg: new Array(32).fill('1').join(''), - kind: CodeKind.PROXY_CLASS -} - export const outputTypeNode: TypeNode = emptyTn('Output') export const lockTypeNode: TypeNode = emptyTn('Lock') @@ -129,8 +121,7 @@ export const BUF_RTID = 1 export const STRING_RTID = 2 export const ARR_HEADER_LENGTH = 16; export const TYPED_ARR_HEADER_LENGTH = 12; -// export const PROXY_OBJ_LENGTH = 8; -// export const OUTPUT_OBJ_LENGTH = 12; -// export const LOCK_OBJ_LENGTH = 12; export const COIN_CLS_PTR = new Pointer(new Uint8Array(32), 0) +// Magic Coin Pkg ID +export const COIN_PKG_ID = new Uint8Array(32).fill(0) diff --git a/packages/vm/test/caller-api.spec.ts b/packages/vm/test/caller-api.spec.ts index 33e0e47a..a57b40d8 100644 --- a/packages/vm/test/caller-api.spec.ts +++ b/packages/vm/test/caller-api.spec.ts @@ -1,18 +1,21 @@ import { - Storage, + MemStorage, VM } from '../src/index.js' import {expect} from 'chai' -import {base16, BufReader, ref} from "@aldea/core"; +import {base16, BufReader, PrivKey, ref} from "@aldea/core"; // @ts-ignore import {fundedExecFactoryFactory, buildVm, ArgsBuilder, parseOutput} from './util.js'; describe('execute txs', () => { - let storage: Storage + let storage: MemStorage let vm: VM let modIdFor: (key: string) => Uint8Array + const userKey = PrivKey.fromRandom() + const userAddr = userKey.toPubKey().toAddress() + let args: ArgsBuilder beforeEach(() => { const data = buildVm(['caller-test-code']) @@ -27,35 +30,41 @@ describe('execute txs', () => { describe('#is', function () { describe('when exact is true', function () { - it('returns true when the caller is the right caller', () => { - const {exec} = fundedExec() + it('returns true when the caller is the right caller', async () => { + const {exec} = await fundedExec() const pkg = exec.import(modIdFor('caller-test-code')) const receiver = exec.instantiate(pkg.idx, ...args.constr('Receiver', [])) const sender = exec.instantiate(pkg.idx, ...args.constr('RightCaller', [])) exec.call(sender.idx, ...args.method('RightCaller', 'doTheCall', [ref(receiver.idx)])) + exec.lockJig(receiver.idx, userAddr) + exec.lockJig(sender.idx, userAddr) + const res = exec.finalize() const parsed = parseOutput(res.outputs[2]) expect(parsed.lastCheck).to.eql("true") }) - it('returns false when the caller is not the right caller', () => { - const {exec} = fundedExec() + it('returns false when the caller is not the right caller', async () => { + const {exec} = await fundedExec() const pkg = exec.import(modIdFor('caller-test-code')) const receiver = exec.instantiate(pkg.idx, ...args.constr('Receiver', [])) const sender = exec.instantiate(pkg.idx, ...args.constr('AnotherCaller', [])) exec.call(sender.idx, ...args.method('AnotherCaller', 'doTheCall', [ref(receiver.idx)])) + exec.lockJig(receiver.idx, userAddr) + exec.lockJig(sender.idx, userAddr) const res = exec.finalize() const parsed = parseOutput(res.outputs[2]) expect(parsed.lastCheck).to.eql("false") }) - it('returns false when the caller is at top level', () => { - const {exec} = fundedExec() + it('returns false when the caller is at top level', async () => { + const {exec} = await fundedExec() const pkg = exec.import(modIdFor('caller-test-code')) const receiver = exec.instantiate(pkg.idx, ...args.constr('Receiver', [])) exec.call(receiver.idx, ...args.method('Receiver', 'checkCallerType', [])) + exec.lockJig(receiver.idx, userAddr) const res = exec.finalize() const parsed = parseOutput(res.outputs[1]) @@ -65,12 +74,14 @@ describe('execute txs', () => { // // This case makes no sense with no interfaces // it('returns true for when an external class is the right one') // - it('returns false when called from subclass', () => { - const {exec} = fundedExec() + it('returns false when called from subclass', async () => { + const {exec} = await fundedExec() const pkg = exec.import(modIdFor('caller-test-code')) const receiver = exec.instantiate(pkg.idx, ...args.constr('Receiver', [])) const sender = exec.instantiate(pkg.idx, ...args.constr('SubclassCaller', [])) exec.call(sender.idx, ...args.method('RightCaller', 'doTheCall', [ref(receiver.idx)])) + exec.lockJig(receiver.idx, userAddr) + exec.lockJig(sender.idx, userAddr) const res = exec.finalize() const reader = new BufReader(res.outputs[2].stateBuf) diff --git a/packages/vm/test/coin.spec.ts b/packages/vm/test/coin.spec.ts index 442161df..9017e502 100644 --- a/packages/vm/test/coin.spec.ts +++ b/packages/vm/test/coin.spec.ts @@ -1,4 +1,4 @@ -import {Storage, VM} from '../src/index.js' +import {MemStorage, VM} from '../src/index.js' import {expect} from 'chai' import {BCS, BufReader, Output, PrivKey, Tx} from "@aldea/core"; import {compile} from "@aldea/compiler"; @@ -9,10 +9,10 @@ import { LockInstruction, SignInstruction } from "@aldea/core/instructions"; -import {COIN_CLS_PTR} from "../src/memory/well-known-abi-nodes.js"; +import {COIN_CLS_PTR} from "../src/well-known-abi-nodes.js"; describe('Coin', () => { - let storage: Storage + let storage: MemStorage let vm: VM // const userPriv = PrivKey.fromRandom() // const userPub = userPriv.toPubKey() @@ -25,7 +25,7 @@ describe('Coin', () => { beforeEach(() => { - storage = new Storage() + storage = new MemStorage() vm = new VM(storage, compile) }) @@ -37,8 +37,8 @@ describe('Coin', () => { let coin: Output let bcs: BCS - beforeEach(() => { - coin = vm.mint(addr, 1000, new Uint8Array(32).fill(1)) + beforeEach(async () => { + coin = await vm.mint(addr, 1000, new Uint8Array(32).fill(1)) const coinPkg = storage.getPkg(COIN_CLS_PTR.id).get() bcs = new BCS(coinPkg.abi) }) diff --git a/packages/vm/test/exec-from-inputs.spec.ts b/packages/vm/test/exec-from-inputs.spec.ts index 48a36ca4..c1f0dc12 100644 --- a/packages/vm/test/exec-from-inputs.spec.ts +++ b/packages/vm/test/exec-from-inputs.spec.ts @@ -3,7 +3,7 @@ // import {buildVm} from "./util.js"; // import {TxExecution} from "../src/tx-execution.js"; // import {ExtendedTx} from "../src/index.js"; -// import {ExTxExecContext} from "../src/tx-context/ex-tx-exec-context.js"; +// import {ExTxExecContext} from "../src/exec-context/ex-tx-exec-context.js"; // import {expect} from "chai"; // import {WasmContainer} from "../src/wasm-container.js"; // import {JigState} from "../src/jig-state.js"; diff --git a/packages/vm/test/new-memory-lift.spec.ts b/packages/vm/test/new-memory-lift.spec.ts index ef781686..98df8061 100644 --- a/packages/vm/test/new-memory-lift.spec.ts +++ b/packages/vm/test/new-memory-lift.spec.ts @@ -1,4 +1,4 @@ -import {Storage} from "../src/index.js"; +import {MemStorage} from "../src/index.js"; import {WasmContainer} from "../src/wasm-container.js"; import {buildVm} from "./util.js"; import {base16, BufWriter, Lock, LockType, Output, Pointer} from "@aldea/core"; @@ -6,13 +6,13 @@ import {expect} from "chai"; import {AbiType} from "../src/memory/abi-helpers/abi-type.js"; import {Option} from "../src/support/option.js"; import {serializeOutput} from "../src/memory/serialize-output.js"; -import {emptyTn} from "../src/memory/well-known-abi-nodes.js"; +import {emptyTn} from "../src/well-known-abi-nodes.js"; import {ValueLifter} from "../src/memory/value-lifter.js"; import {JigData, LowerValue} from "../src/memory/lower-value.js"; describe('NewMemoryLower', () => { let modIdFor: (key: string) => Uint8Array - let storage: Storage; + let storage: MemStorage; let container: WasmContainer; let jigData: Map diff --git a/packages/vm/test/new-memory-lower.spec.ts b/packages/vm/test/new-memory-lower.spec.ts index a5e83574..1766853d 100644 --- a/packages/vm/test/new-memory-lower.spec.ts +++ b/packages/vm/test/new-memory-lower.spec.ts @@ -1,4 +1,4 @@ -import {Storage} from "../src/index.js"; +import {MemStorage} from "../src/index.js"; import {WasmContainer} from "../src/wasm-container.js"; import {buildVm} from "./util.js"; import {Address, base16, BCS, BufReader, BufWriter, Lock, LockType, Output, Pointer} from "@aldea/core"; @@ -9,7 +9,7 @@ import {Option} from "../src/support/option.js"; import {PublicLock} from "../src/locks/public-lock.js"; import {serializeOutput} from "../src/memory/serialize-output.js"; import {AddressLock} from "../src/locks/address-lock.js"; -import {BUF_RTID, emptyTn, STRING_RTID} from "../src/memory/well-known-abi-nodes.js"; +import {BUF_RTID, emptyTn, STRING_RTID} from "../src/well-known-abi-nodes.js"; import {JigData, LowerValue} from "../src/memory/lower-value.js"; @@ -43,7 +43,7 @@ function checkLock (container: WasmContainer, lockPtr: WasmWord, extOrigin: Uint describe('NewMemoryLower', () => { let modIdFor: (key: string) => Uint8Array - let storage: Storage; + let storage: MemStorage; let container: WasmContainer; let jigData: Map @@ -252,7 +252,7 @@ describe('NewMemoryLower', () => { const bufContent = new Uint8Array([0,1,2,3,4,5,6,7,8,9]); buf.writeBytes(bufContent) - const ty = AbiType.fromName('ArrayBuffer') + const ty = AbiType.buffer() let ptr = target.lower(buf.data, ty) let objBuf = container.mem.extract(ptr.minus(8), 18) @@ -313,7 +313,7 @@ describe('NewMemoryLower', () => { const aString = "this is a string"; buf.writeBytes(new Uint8Array(Buffer.from(aString))) - const ty = AbiType.fromName('string') + const ty = AbiType.string() let ptr = target.lower(buf.data, ty) let header = container.mem.read(ptr.minus(8), 8) diff --git a/packages/vm/test/package-id-test.spec.ts b/packages/vm/test/package-id-test.spec.ts index 0c786234..5b001d92 100644 --- a/packages/vm/test/package-id-test.spec.ts +++ b/packages/vm/test/package-id-test.spec.ts @@ -1,5 +1,5 @@ import {expect} from "chai"; -import {calculatePackageId} from "../src/index.js"; +import {calculatePackageHash} from "../src/index.js"; describe('calculatePackageId', function () { const file1 = ` @@ -17,7 +17,7 @@ describe('calculatePackageId', function () { const sources2 = new Map() sources2.set(entries[0], file2) - expect(calculatePackageId(entries, sources1)).not.to.eql(calculatePackageId(entries, sources2)) + expect(calculatePackageHash(entries, sources1)).not.to.eql(calculatePackageHash(entries, sources2)) }) it('returns different values for different entry points', () => { @@ -27,7 +27,7 @@ describe('calculatePackageId', function () { sources.set(entries1[0], file1) sources.set(entries2[0], file1) - expect(calculatePackageId(entries1, sources)).not.to.eql(calculatePackageId(entries2, sources)) + expect(calculatePackageHash(entries1, sources)).not.to.eql(calculatePackageHash(entries2, sources)) }) it('is not affected by entries order', () => { @@ -37,7 +37,7 @@ describe('calculatePackageId', function () { sources.set(entries1[0], file1) sources.set(entries1[1], file1) - expect(calculatePackageId(entries1, sources)).to.eql(calculatePackageId(entries2, sources)) + expect(calculatePackageHash(entries1, sources)).to.eql(calculatePackageHash(entries2, sources)) }) it('is not affected by source code adition order', () => { @@ -50,6 +50,6 @@ describe('calculatePackageId', function () { sources2.set(entries[1], file1) sources2.set(entries[0], file1) - expect(calculatePackageId(entries, sources1)).to.eql(calculatePackageId(entries, sources2)) + expect(calculatePackageHash(entries, sources1)).to.eql(calculatePackageHash(entries, sources2)) }) }); diff --git a/packages/vm/test/specific-scenarios.spec.ts b/packages/vm/test/specific-scenarios.spec.ts index 9ec510c2..40bbf950 100644 --- a/packages/vm/test/specific-scenarios.spec.ts +++ b/packages/vm/test/specific-scenarios.spec.ts @@ -1,15 +1,15 @@ -import {Storage, VM} from '../src/index.js' +import {MemStorage, VM} from '../src/index.js' import {expect} from 'chai' import {PrivKey, PubKey, ref} from "@aldea/core"; import {ArgsBuilder, buildVm, fundedExecFactoryFactory, parseOutput} from "./util.js"; import {TxExecution} from "../src/tx-execution.js"; -import {StorageTxContext} from "../src/tx-context/storage-tx-context.js"; +import {StorageTxContext} from "../src/exec-context/storage-tx-context.js"; import {randomBytes} from "@aldea/core/support/util"; -import {ExecOpts} from "../src/export-opts.js"; +import {ExecOpts} from "../src/exec-opts.js"; import { GAME, KITCHEN_SINK, SELL_OFFER } from './explorer-examples.js' describe('execute txs', () => { - let storage: Storage + let storage: MemStorage let vm: VM const userPriv = PrivKey.fromRandom() const userPub = userPriv.toPubKey() @@ -43,21 +43,21 @@ describe('execute txs', () => { beforeEach(async () => { - const { exec: exec1 } = fundedExec() + const { exec: exec1 } = await fundedExec() await exec1.deploy(['entry.ts'], new Map([['entry.ts', gameSrc]])) const res1 = await exec1.finalize() storage.persistExecResult(res1) gameArgs = new ArgsBuilder(res1.deploys[0].abi) gamePkgId = res1.deploys[0].hash - const { exec: exec2 } = fundedExec() + const { exec: exec2 } = await fundedExec() await exec2.deploy(['entry.ts'], new Map([['entry.ts', kitchenSrc]])) const res2 = await exec2.finalize() storage.persistExecResult(res2) kitchenArgs = new ArgsBuilder(res2.deploys[0].abi) kitchenPkgId = res2.deploys[0].hash - const { exec: exec3 } = fundedExec() + const { exec: exec3 } = await fundedExec() await exec3.deploy(['entry.ts'], new Map([['entry.ts', sellOfferSrc]])) const res3 = await exec3.finalize() storage.persistExecResult(res3) @@ -65,10 +65,10 @@ describe('execute txs', () => { sellPkgId = res3.deploys[0].hash }) - it('works', () => { - const { exec } = fundedExec([userPriv]) - const minted1 = vm.mint(userAddr, 100000) - const minted2 = vm.mint(userAddr, 10001) + it('works', async () => { + const { exec } = await fundedExec([userPriv]) + const minted1 = await vm.mint(userAddr, 100000) + const minted2 = await vm.mint(userAddr, 10001) let gamePkg = exec.import(gamePkgId) exec.import(kitchenPkgId) diff --git a/packages/vm/test/tx-execution.spec.ts b/packages/vm/test/tx-execution.spec.ts index 0854fb3d..c13669ae 100644 --- a/packages/vm/test/tx-execution.spec.ts +++ b/packages/vm/test/tx-execution.spec.ts @@ -1,19 +1,22 @@ -import {Storage, VM} from '../src/index.js' +import {ExtendedTx, MemStorage, VM} from '../src/index.js' import {expect} from 'chai' -import {base16, BCS, BufReader, LockType, Output, Pointer, PrivKey, PubKey, ref} from "@aldea/core"; +import {base16, BCS, BufReader, LockType, Output, Pointer, PrivKey, PubKey, ref, Tx} from "@aldea/core"; import {Abi} from '@aldea/core/abi'; import {ArgsBuilder, buildVm, fundedExecFactoryFactory, parseOutput} from "./util.js"; -import {COIN_CLS_PTR} from "../src/memory/well-known-abi-nodes.js"; +import {COIN_CLS_PTR} from "../src/well-known-abi-nodes.js"; import {ExecutionError, PermissionError} from "../src/errors.js"; import {TxExecution} from "../src/tx-execution.js"; import {StatementResult} from "../src/statement-result.js"; import {ExecutionResult} from "../src/index.js"; -import {StorageTxContext} from "../src/tx-context/storage-tx-context.js"; +import {StorageTxContext} from "../src/exec-context/storage-tx-context.js"; import {randomBytes} from "@aldea/core/support/util"; -import {ExecOpts} from "../src/export-opts.js"; +import {ExecOpts} from "../src/exec-opts.js"; +import {ExTxExecContext} from "../src/exec-context/ex-tx-exec-context.js"; +import {SignInstruction} from "@aldea/core/instructions"; +import {compile} from "@aldea/compiler"; describe('execute txs', () => { - let storage: Storage + let storage: MemStorage let vm: VM const userPriv = PrivKey.fromRandom() const userPub = userPriv.toPubKey() @@ -61,8 +64,8 @@ describe('execute txs', () => { } - it('instantiate creates the right output', () => { - const {exec, txHash} = fundedExec() + it('instantiate creates the right output', async () => { + const {exec, txHash} = await fundedExec() const mod = exec.import(modIdFor('flock')) const instanceIndex = exec.instantiate(mod.idx, 0, new Uint8Array([0])) exec.lockJig(instanceIndex.idx, userAddr) @@ -75,8 +78,8 @@ describe('execute txs', () => { expect(output.lock.data).to.eql(userAddr.hash) }) - it('sends arguments to constructor properly.', () => { - const {exec} = fundedExec() + it('sends arguments to constructor properly.', async () => { + const {exec} = await fundedExec() const module = exec.import(modIdFor('weapon')) // index 0 const bcs = new BCS(abiFor('weapon')); const argBuf = bcs.encode('Weapon_constructor', ['Sable Corvo de San Martín', 100000]) @@ -89,8 +92,8 @@ describe('execute txs', () => { expect(parsed[1]).to.eql(100000) }) - it('can call methods on jigs', () => { - const {exec} = fundedExec() + it('can call methods on jigs', async () => { + const {exec} = await fundedExec() const importIndex = exec.import(modIdFor('flock')) const flock = exec.instantiate(importIndex.idx, 0, new Uint8Array([0])) exec.call(flock.idx, 1, new Uint8Array([0])) @@ -101,8 +104,8 @@ describe('execute txs', () => { expect(props['size']).to.eql(1) }) - it('can make calls on jigs sending basic parameters', () => { - const {exec} = fundedExec() + it('can make calls on jigs sending basic parameters', async () => { + const {exec} = await fundedExec() const pkg = exec.import(modIdFor('flock')) const flock = exec.instantiate(pkg.idx, 0, new Uint8Array([0])) exec.call(flock.idx, 2, new Uint8Array([0, 7, 0, 0, 0])) @@ -114,8 +117,8 @@ describe('execute txs', () => { }) - it('can make calls on jigs sending jigs as parameters', () => { - const {exec} = fundedExec() + it('can make calls on jigs sending jigs as parameters', async () => { + const {exec} = await fundedExec() const flockWasm = exec.import(modIdFor('flock')) const counterWasm = exec.import(modIdFor('sheep-counter')) const flock = exec.instantiate(flockWasm.idx, ...flockArgs.constr('Flock', [])) @@ -133,8 +136,8 @@ describe('execute txs', () => { expect(o['legCount']).to.eql(8) }) - it('after locking a jig in the code the state gets updated properly', () => { - const {exec} = fundedExec() + it('after locking a jig in the code the state gets updated properly', async () => { + const {exec} = await fundedExec() const flockMod = exec.import(modIdFor('flock')) const counterMod = exec.import(modIdFor('sheep-counter')) const flock = exec.instantiate(flockMod.idx, ...flockArgs.constr('Flock', [])) @@ -155,8 +158,8 @@ describe('execute txs', () => { shepherd: StatementResult } - function shepherdExec (privKeys: PrivKey[] = []): ShepExec { - const {exec} = fundedExec(privKeys) + async function shepherdExec (privKeys: PrivKey[] = []): Promise { + const {exec} = await fundedExec(privKeys) const flockWasm = exec.import(modIdFor('flock')) const counterWasm = exec.import(modIdFor('sheep-counter')) const flock = exec.instantiate(flockWasm.idx, ...flockArgs.constr('Flock', [])) @@ -171,15 +174,15 @@ describe('execute txs', () => { } } - it('accessing class ptr works', () => { + it('accessing class ptr works', async () => { const { exec, - } = shepherdExec([userPriv]) + } = await shepherdExec([userPriv]) const result = exec.finalize() - storage.persistExecResult(result) + await storage.persistExecResult(result) - const {exec: exec2} = fundedExec([userPriv]) + const {exec: exec2} = await fundedExec([userPriv]) const loaded = exec2.load(result.outputs[2].hash) const classPtrStmt = exec2.call(loaded.idx, ...ctrArgs.method('Shepherd', 'myClassPtr', [])) const flockClassPtrStmt = exec2.call(loaded.idx, ...ctrArgs.method('Shepherd', 'flockClassPtr', [])) @@ -191,8 +194,8 @@ describe('execute txs', () => { expect(r2.readBytes()).to.eql(new Pointer(modIdFor('flock'), 0).toBytes()) }) - it('fails if the tx is trying to lock an already locked jig', () => { - const {exec, flock, shepherd} = shepherdExec() + it('fails if the tx is trying to lock an already locked jig', async () => { + const {exec, flock, shepherd} = await shepherdExec() expect(() => { exec.lockJig(flock.idx, userAddr) @@ -209,8 +212,8 @@ describe('execute txs', () => { ) }) - it('fails when a jig tries to lock a locked jig', () => { - const {exec} = fundedExec() + it('fails when a jig tries to lock a locked jig', async () => { + const {exec} = await fundedExec() const flockWasm = exec.import(modIdFor('flock')) const counterWasm = exec.import(modIdFor('sheep-counter')) const flock = exec.instantiate(flockWasm.idx, ...flockArgs.constr('Flock', [])) @@ -231,8 +234,8 @@ describe('execute txs', () => { ant3: StatementResult } - function antExec (): AntExec { - const {exec} = fundedExec() + async function antExec (): Promise { + const {exec} = await fundedExec() const antWasm = exec.import(modIdFor('ant')) const ant1 = exec.instantiate(antWasm.idx, ...antArgs.constr('Ant', [])) const ant2 = exec.instantiate(antWasm.idx, ...antArgs.constr('Ant', [])) @@ -245,37 +248,49 @@ describe('execute txs', () => { return {exec, ant1, ant2, ant3} } - it('fails when a jig tries to call a method on a jig of the same class with no permissions', () => { - const {exec, ant1} = antExec() + it('fails when a jig tries to call a method on a jig of the same class with no permissions', async () => { + const {exec, ant1} = await antExec() expect(() => exec.call(ant1.idx, ...antArgs.method('Ant', 'forceAFriendToWork', [])) // calls public method on not owned jig ).to.throw(PermissionError) }) - it('fails when a jig tries to call a protected method on another jig of the same module that does not own', () => { - const {exec, ant1} = antExec() + it('fails when a jig tries to call a protected method on another jig of the same module that does not own', async () => { + const {exec, ant1} = await antExec() expect(() => exec.call(ant1.idx, ...antArgs.method('Ant', 'forceFriendsFamilyToWork', [])) // calls private method on not owned jig ).to.throw(PermissionError) }) - describe('when there is a frozen jig', () => { + describe('when there is a frozen jig', async () => { let frozenOutput: Output - beforeEach(() => { - const {exec} = fundedExec() + beforeEach(async () => { + const {exec} = await fundedExec() const flockMod = exec.import(modIdFor('flock')) const flock = exec.instantiate(flockMod.idx, ...flockArgs.constr('Flock', [])) exec.call(flock.idx, ...flockArgs.method('Flock', 'goToFridge', [])) const res = exec.finalize() expect(res.outputs[1].lock.type).to.eql(LockType.FROZEN) frozenOutput = res.outputs[1] - storage.persistExecResult(res) + await storage.persistExecResult(res) }) it('cannot be called methods', () => { - const {exec} = fundedExec() + const tx = new Tx() + tx.push(new SignInstruction(new Uint8Array(64).fill(0), userPub.toBytes())) + const container = storage.wasmForPackageId(frozenOutput.classPtr.id).get() + + const exec = new TxExecution( + new ExTxExecContext( + new ExtendedTx(tx, [frozenOutput]), + [container], + compile + ), + ExecOpts.default() + ) + const jig = exec.load(frozenOutput.hash) expect(() => exec.call(jig.idx, ...flockArgs.method('Flock', 'grow', []))).to .throw(PermissionError, @@ -283,7 +298,19 @@ describe('execute txs', () => { }) it('cannot be locked', () => { - const {exec} = fundedExec() + const tx = new Tx() + tx.push(new SignInstruction(new Uint8Array(64).fill(0), userPub.toBytes())) + const container = storage.wasmForPackageId(frozenOutput.classPtr.id).get() + + const exec = new TxExecution( + new ExTxExecContext( + new ExtendedTx(tx, [frozenOutput]), + [container], + compile + ), + ExecOpts.default() + ) + const jig = exec.load(frozenOutput.hash) expect(() => exec.lockJig(jig.idx, userAddr)).to .throw(PermissionError, @@ -291,12 +318,12 @@ describe('execute txs', () => { }) }); - it('can load an existing jig', () => { - const {exec: exec1} = antExec() + it('can load an existing jig', async () => { + const {exec: exec1} = await antExec() const res1 = exec1.finalize(); - storage.persistExecResult(res1) + await storage.persistExecResult(res1) - const {exec: exec2} = fundedExec([userPriv]) + const {exec: exec2} = await fundedExec([userPriv]) const loaded = exec2.load(res1.outputs[1].hash) exec2.call(loaded.idx, ...antArgs.method('Ant', 'doExercise', [])) @@ -307,12 +334,12 @@ describe('execute txs', () => { expect(antProps['ownForce']).to.eql(2) }) - it('can load an existing jig by origin', () => { - const {exec: exec1} = antExec() + it('can load an existing jig by origin', async () => { + const {exec: exec1} = await antExec() const res1 = exec1.finalize(); - storage.persistExecResult(res1) + await storage.persistExecResult(res1) - const {exec: exec2} = fundedExec([userPriv]) + const {exec: exec2} = await fundedExec([userPriv]) const loaded = exec2.loadByOrigin(res1.outputs[1].origin.toBytes()) exec2.call(loaded.idx, ...antArgs.method('Ant', 'doExercise', [])) @@ -323,28 +350,28 @@ describe('execute txs', () => { expect(antProps['ownForce']).to.eql(2) }) - it('fails when try to load an unknown jig', () => { - const {exec} = fundedExec() + it('fails when try to load an unknown jig', async () => { + const {exec} = await fundedExec() expect(() => { exec.load(new Uint8Array(32).fill(1)) }).to.throw(ExecutionError) }) - it('fails when try to load by an unknown origin', () => { - const {exec} = fundedExec() + it('fails when try to load by an unknown origin', async () => { + const {exec} = await fundedExec() expect(() => { exec.loadByOrigin(new Uint8Array(34).fill(1)) }).to.throw(ExecutionError) }) - it('can load jigs that include proxies to other packages', () => { - const {exec: exec1} = shepherdExec() + it('can load jigs that include proxies to other packages', async () => { + const {exec: exec1} = await shepherdExec() const res1 = exec1.finalize() - storage.persistExecResult(res1) + await storage.persistExecResult(res1) - const {exec: exec2} = fundedExec([userPriv]) + const {exec: exec2} = await fundedExec([userPriv]) const jig = exec2.load(res1.outputs[2].hash) const value = exec2.call(jig.idx, ...ctrArgs.method('Shepherd', 'sheepCount', [])) expect(value.asValue().ptr.toUInt()).to.eql(1) @@ -352,8 +379,8 @@ describe('execute txs', () => { expect(res2.outputs).to.have.length(2) }) - function flockBagExec (): ExecutionResult { - const {exec} = fundedExec() + async function flockBagExec (): Promise { + const {exec} = await fundedExec() const flockWasm = exec.import(modIdFor('flock')) const flock = exec.exec(flockWasm.idx, ...flockArgs.exec('flockWithSize', [3])) const bag = exec.instantiate(flockWasm.idx, ...flockArgs.constr('FlockBag', [])) @@ -362,8 +389,8 @@ describe('execute txs', () => { return exec.finalize() } - it('can send static method result as parameter', () => { - const result = flockBagExec() + it('can send static method result as parameter', async () => { + const result = await flockBagExec() const flockState = parseOutput(result.outputs[1]) expect(flockState.size).to.eql(3) @@ -371,13 +398,13 @@ describe('execute txs', () => { expect(bagState.flocks).to.eql([result.outputs[1].origin]) }) - it('when a child jig is not used it does not appear in the outputs', () => { - const res1 = flockBagExec() - storage.persistExecResult(res1) + it('when a child jig is not used it does not appear in the outputs', async () => { + const res1 = await flockBagExec() + await storage.persistExecResult(res1) const anotherKey = PrivKey.fromRandom() - const {exec} = fundedExec([userPriv]) + const {exec} = await fundedExec([userPriv]) const loadedJig = exec.load(res1.outputs[2].hash) exec.lockJig(loadedJig.idx, anotherKey.toPubKey().toAddress()) @@ -397,8 +424,8 @@ describe('execute txs', () => { it('can return types with nested jigs') - it('can call exported functions from inside jigs', () => { - const {exec} = fundedExec() + it('can call exported functions from inside jigs', async () => { + const {exec} = await fundedExec() const flockWasm = exec.import(modIdFor('flock')) const flock = exec.instantiate(flockWasm.idx, ...flockArgs.constr('Flock', [])) exec.call(flock.idx, ...flockArgs.method('Flock', 'groWithExternalFunction', [])) @@ -409,8 +436,8 @@ describe('execute txs', () => { expect(parsed.size).to.eql(1) }) - it('saves entire state for jigs using inheritance', () => { - const {exec} = fundedExec() + it('saves entire state for jigs using inheritance', async () => { + const {exec} = await fundedExec() const wasm = exec.import(modIdFor('sheep')) const sheep = exec.instantiate(wasm.idx, ...sheepArgs.constr('MutantSheep', ['Wolverine', 'black'])) exec.lockJig(sheep.idx, userAddr) @@ -423,8 +450,8 @@ describe('execute txs', () => { expect(reader.readU32()).to.eql(0) }) - it('can call base class and concrete class methods', () => { - const {exec} = fundedExec() + it('can call base class and concrete class methods', async () => { + const {exec} = await fundedExec() const wasm = exec.import(modIdFor('sheep')) const sheep = exec.instantiate(wasm.idx, ...sheepArgs.constr('MutantSheep', ['Wolverine', 'black'])) exec.call(sheep.idx, ...sheepArgs.method('Sheep', 'chopOneLeg', [])) @@ -441,15 +468,15 @@ describe('execute txs', () => { expect(reader.readU32()).to.eql(10) }) - it('can create, lock and reload a jig that uses inheritance', () => { - const {exec: exec1} = fundedExec() + it('can create, lock and reload a jig that uses inheritance', async () => { + const {exec: exec1} = await fundedExec() const wasm = exec1.import(modIdFor('sheep')) const sheep = exec1.instantiate(wasm.idx, ...sheepArgs.constr('MutantSheep', ['Wolverine', 'black'])) exec1.lockJig(sheep.idx, userAddr) const res1 = exec1.finalize() - storage.persistExecResult(res1) + await storage.persistExecResult(res1) - const {exec: exec2} = fundedExec([userPriv]) + const {exec: exec2} = await fundedExec([userPriv]) const loaded = exec2.load(res1.outputs[1].hash) exec2.call(loaded.idx, ...sheepArgs.method('Sheep', 'chopOneLeg', [])) // -1 leg exec2.call(loaded.idx, ...sheepArgs.method('MutantSheep', 'regenerateLeg', [])) // +1 leg @@ -463,27 +490,27 @@ describe('execute txs', () => { expect(reader.readU32()).to.eql(10) }) - it('coin eater', () => { + it('coin eater', async () => { // const txHash = new Uint8Array(32).fill(10) // const context = new StorageTxContext(txHash, [userPriv.toPubKey()], storage, vm) - const {exec} = fundedExec([userPriv]) + const {exec} = await fundedExec([userPriv]) - const mintedCoin = vm.mint(userAddr, 1000) + const mintedCoin = await vm.mint(userAddr, 1000) const wasm = exec.import(modIdFor('coin-eater')) const coin = exec.load(mintedCoin.hash) const eater = exec.instantiate(wasm.idx, ...coinEaterArgs.constr('CoinEater', [ref(coin.idx)])) exec.lockJig(eater.idx, userAddr) const ret = exec.finalize() - storage.persistExecResult(ret) + await storage.persistExecResult(ret) const eaterState = parseOutput(ret.outputs[1]) expect(eaterState.lastCoin).to.eql(mintedCoin.origin) expect(eaterState.otherCoins).to.eql([]) }) - it('keeps locking state up to date after lock', () => { - const {exec} = fundedExec([userPriv]) + it('keeps locking state up to date after lock', async () => { + const {exec} = await fundedExec([userPriv]) const wasm = exec.import(modIdFor('flock')) const flock = exec.instantiate(wasm.idx, ...flockArgs.constr('Flock', [])) @@ -499,7 +526,7 @@ describe('execute txs', () => { it('can compile', async () => { - const {exec} = fundedExec() + const {exec} = await fundedExec() const fileContent = 'export class Dummy extends Jig {}' const entries = ['main.ts'] const files = new Map([['main.ts', fileContent]]) @@ -511,18 +538,18 @@ describe('execute txs', () => { expect(res.deploys[0].sources).to.eql(files) }) - it('fails if not enough fees there', () => { + it('fails if not enough fees there', async () => { const txHash = new Uint8Array(32).fill(10) const context = new StorageTxContext(txHash, [userPriv.toPubKey()], storage, vm) const exec = new TxExecution(context, ExecOpts.default()) - const coin = vm.mint(userAddr, 10) + const coin = await vm.mint(userAddr, 10) const stmt = exec.load(coin.hash) exec.fund(stmt.idx) expect(() => exec.finalize()).to.throw(ExecutionError, 'Not enough funding. Provided: 10. Needed: 100') }) - it('generates empty statements for sign and signto', () => { - const {exec} = fundedExec() + it('generates empty statements for sign and signto', async () => { + const {exec} = await fundedExec() const stmt1 = exec.sign(new Uint8Array(64).fill(0), new Uint8Array(32).fill(1)) const stmt2 = exec.signTo(new Uint8Array(64).fill(2), new Uint8Array(32).fill(3)) @@ -532,9 +559,12 @@ describe('execute txs', () => { expect(() => stmt2.asContainer()).to.throw() }) - it('when call an external constructor a new jig is created a right proxy gets assigned', () => { - const {exec, shepherd} = shepherdExec([userPriv]) + it('when call an external constructor a new jig is created a right proxy gets assigned', async () => { + const {exec, flock, shepherd} = await shepherdExec([userPriv]) exec.call(shepherd.idx, ...ctrArgs.method('Shepherd', 'breedANewFlock', [5])) + exec.lockJig(flock.idx, userAddr) + exec.lockJig(shepherd.idx, userAddr) + const res = exec.finalize() expect(res.outputs).to.have.length(4) @@ -542,9 +572,9 @@ describe('execute txs', () => { expect(parsedFlock.size).to.eql(5) }) - it('input does not include newly created jigs', () => { + it('input does not include newly created jigs', async () => { const exec = emptyExec([userPub]) // this loads a coin - const minted = vm.mint(userAddr, 100) + const minted = await vm.mint(userAddr, 100) const imported = exec.import(modIdFor('flock')) const jig = exec.instantiate(imported.idx, 0, new Uint8Array([0])) exec.lockJig(jig.idx, userAddr) @@ -557,15 +587,15 @@ describe('execute txs', () => { expect(res.spends[0].id).to.eql(minted.id) }) - it('result includes reads', () => { - const {exec: exec1} = fundedExec([]) // this loads a coin + it('result includes reads', async () => { + const {exec: exec1} = await fundedExec([]) // this loads a coin const pkg = exec1.import(modIdFor('sheep')) const sheep = exec1.instantiate(pkg.idx, ...sheepArgs.constr('Sheep', ['Baa', 'black'])) exec1.lockJig(sheep.idx, userAddr) const res1 = exec1.finalize(); - storage.persistExecResult(res1) + await storage.persistExecResult(res1) - const {exec: exec2} = fundedExec([]) // this loads a coin + const {exec: exec2} = await fundedExec([]) // this loads a coin const loaded = exec2.load(res1.outputs[1].hash) const imported = exec2.import(modIdFor('sheep')) const cloned = exec2.exec(imported.idx, ...sheepArgs.exec('clone', [ref(loaded.idx)])) @@ -576,15 +606,15 @@ describe('execute txs', () => { expect(res2.reads[0].hash).to.eql(res1.outputs[1].hash) }) - it('when a read was also spend it does not appear in spends', () => { - const {exec: exec1} = fundedExec([]) // this loads a coin + it('when a read was also spend it does not appear in spends', async () => { + const {exec: exec1} = await fundedExec([]) // this loads a coin const pkg = exec1.import(modIdFor('sheep')) const sheep = exec1.instantiate(pkg.idx, ...sheepArgs.constr('Sheep', ['Baa', 'black'])) exec1.lockJig(sheep.idx, userAddr) const res1 = exec1.finalize(); - storage.persistExecResult(res1) + await storage.persistExecResult(res1) - const {exec: exec2} = fundedExec([userPriv]) // this loads a coin + const {exec: exec2} = await fundedExec([userPriv]) // this loads a coin const loaded = exec2.load(res1.outputs[1].hash) const imported = exec2.import(modIdFor('sheep')) const cloned = exec2.exec(imported.idx, ...sheepArgs.exec('clone', [ref(loaded.idx)])) @@ -620,40 +650,44 @@ describe('execute txs', () => { } ` - const {exec: exec1} = fundedExec() + const {exec: exec1} = await fundedExec() await exec1.deploy(['a.ts'], new Map([['a.ts', src]])) const res = exec1.finalize() pkgHash = res.deploys[0].hash - storage.persistExecResult(res) + await storage.persistExecResult(res) }) - it('understand null as null', () => { - const { exec } = fundedExec() + it('understand null as null', async () => { + const { exec } = await fundedExec() let pkgStmt = exec.import(pkgHash) - exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0, 0])) + let stmt = exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0, 0])) + exec.lockJig(stmt.idx, userAddr) const res = exec.finalize() expect(res.outputs[1].stateBuf).to.eql(new Uint8Array([0, 0])) }) - it('understand present values as the value', () => { - const { exec } = fundedExec() + it('understand present values as the value', async () => { + const { exec } = await fundedExec() let pkgStmt = exec.import(pkgHash) - exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0, 1, 5, 104, 101, 108, 108, 111])) + let stmt = exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0, 1, 5, 104, 101, 108, 108, 111])) + exec.lockJig(stmt.idx, userAddr) const res = exec.finalize() expect(res.outputs[1].stateBuf).to.eql(new Uint8Array([1, 5, 104, 101, 108, 108, 111, 0])) }) - it('is correctly interpreted by asc', () => { - const { exec } = fundedExec() + it('is correctly interpreted by asc', async () => { + const { exec } = await fundedExec() const pkgStmt = exec.import(pkgHash) const jig1 = exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0, 1, 5, 104, 101, 108, 108, 111])) const jig2 = exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0, 0])) exec.call(jig1.idx, 0, new Uint8Array([0])) exec.call(jig2.idx, 0, new Uint8Array([0])) + exec.lockJig(jig1.idx, userAddr) + exec.lockJig(jig2.idx, userAddr) const res = exec.finalize() expect(res.outputs[1].stateBuf).to.eql(new Uint8Array([1, 5, 104, 101, 108, 108, 111, 2])) @@ -682,31 +716,32 @@ describe('execute txs', () => { } ` - const {exec: exec1} = fundedExec() + const {exec: exec1} = await fundedExec() await exec1.deploy(['a.ts'], new Map([['a.ts', src]])) const res = exec1.finalize() pkgHash = res.deploys[0].hash pkgAbi = res.deploys[0].abi - storage.persistExecResult(res) + await storage.persistExecResult(res) }) - it('can lower and lift bigints', () => { - const { exec } = fundedExec() + it('can lower and lift bigints', async () => { + const { exec } = await fundedExec() let pkgStmt = exec.import(pkgHash) const bcs = new BCS(pkgAbi); const buf = bcs.encode('BigInt', 10n) const jigStmt = exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0, ...buf])) exec.call(jigStmt.idx, 0, new Uint8Array([0])) + exec.lockJig(jigStmt.idx, userAddr) const res = exec.finalize() const parsed = bcs.decode('A', res.outputs[1].stateBuf) expect(parsed[0]).to.eql(10n) expect(parsed[1]).to.eql(11n) }) - it('can lower and lift big bigints', () => { - const { exec } = fundedExec() + it('can lower and lift big bigints', async () => { + const { exec } = await fundedExec() let pkgStmt = exec.import(pkgHash) const bcs = new BCS(pkgAbi); @@ -714,14 +749,15 @@ describe('execute txs', () => { const buf = bcs.encode('BigInt', value) const jigStmt = exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0, ...buf])) exec.call(jigStmt.idx, 0, new Uint8Array([0])) + exec.lockJig(jigStmt.idx, userAddr) const res = exec.finalize() const parsed = bcs.decode('A', res.outputs[1].stateBuf) expect(parsed[0]).to.eql(value) expect(parsed[1]).to.eql(value + 1n) }) - it('can lower and lift big big bigints', () => { - const { exec } = fundedExec() + it('can lower and lift big big bigints', async () => { + const { exec } = await fundedExec() let pkgStmt = exec.import(pkgHash) const bcs = new BCS(pkgAbi); @@ -729,6 +765,7 @@ describe('execute txs', () => { const buf = bcs.encode('BigInt', value) const jigStmt = exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0, ...buf])) exec.call(jigStmt.idx, 0, new Uint8Array([0])) + exec.lockJig(jigStmt.idx, userAddr) const res = exec.finalize() const parsed = bcs.decode('A', res.outputs[1].stateBuf) expect(parsed[0]).to.eql(value) @@ -738,10 +775,10 @@ describe('execute txs', () => { type WithPackageResult = { pkgHash: Uint8Array, pkgAbi: Abi } async function withPackage(src: string): Promise { - const {exec: exec1} = fundedExec() + const {exec: exec1} = await fundedExec() await exec1.deploy(['a.ts'], new Map([['a.ts', src]])) const res = exec1.finalize() - storage.persistExecResult(res) + await storage.persistExecResult(res) return {pkgHash: res.deploys[0].hash, pkgAbi: res.deploys[0].abi} @@ -766,11 +803,11 @@ describe('execute txs', () => { args = new ArgsBuilder(pkgAbi) }) - it('ends with an error when gas usage goes over config.', () => { + it('ends with an error when gas usage goes over config.', async () => { const opts = ExecOpts.default() opts.wasmExecutionMaxHydros = 1n opts.wasmExecutionHydroSize = 10000n - const { exec } = fundedExec([], opts) + const { exec } = await fundedExec([], opts) let pkgStmt = exec.import(pkgHash) const jig = exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0])) @@ -780,11 +817,11 @@ describe('execute txs', () => { ).to.throw(ExecutionError, 'Max hydros for Raw Execution (1) was over passed') }) - it('does not throw when gas usage is big enough.', () => { + it('does not throw when gas usage is big enough.', async () => { const opts = ExecOpts.default() opts.wasmExecutionMaxHydros = 2n opts.wasmExecutionHydroSize = 1000000n - const { exec } = fundedExec([], opts) + const { exec } = await fundedExec([], opts) let pkgStmt = exec.import(pkgHash) const jig = exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0])) @@ -795,14 +832,14 @@ describe('execute txs', () => { }) }) - it('a basic execution uses 5 hydros ', () => { + it('a basic execution uses 5 hydros ', async () => { // 1 container // 1 signature // 1 unit of data moved // 1 unit of raw execution // 1 new output created - const {exec} = fundedExec() + const {exec} = await fundedExec() const mod = exec.import(modIdFor('flock')) const instanceIndex = exec.instantiate(mod.idx, 0, new Uint8Array([0])) exec.lockJig(instanceIndex.idx, userAddr) @@ -851,11 +888,11 @@ describe('execute txs', () => { senderArgs = new ArgsBuilder(abi) }) - it('throws if data transfer is bigger than conf limit', () => { + it('throws if data transfer is bigger than conf limit', async () => { const opts = ExecOpts.default(); opts.moveDataMaxHydros = 3n opts.moveDataHydroSize = 500n // 1.5k is not enough because data is lifted and lowered - const {exec} = fundedExec([], opts) + const {exec} = await fundedExec([], opts) const receiverCont = exec.import(receiverHash) const senderCont = exec.import(senderHash) const receiver = exec.instantiate(receiverCont.idx, 0, new Uint8Array([0])) @@ -866,10 +903,10 @@ describe('execute txs', () => { ).to.throw(ExecutionError, 'Max hydros for Moved Data (3) was over passed') }) - it('works if transfer is big enough', () => { + it('works if transfer is big enough', async () => { const opts = ExecOpts.default(); opts.moveDataMaxHydros = 2050n // It needs at least 2k because data has to be lifted and lowered - const {exec} = fundedExec([], opts) + const {exec} = await fundedExec([], opts) const receiverCont = exec.import(receiverHash) const senderCont = exec.import(senderHash) const receiver = exec.instantiate(receiverCont.idx, 0, new Uint8Array([0])) @@ -880,34 +917,36 @@ describe('execute txs', () => { ).not.to.throw() }) - it('adds hidros according to the data transfered', () => { + it('adds hidros according to the data transfered', async () => { const opts = ExecOpts.default(); opts.moveDataHydroSize = 100n; - const {exec} = fundedExec([], opts) + const {exec} = await fundedExec([], opts) const receiverCont = exec.import(receiverHash) const senderCont = exec.import(senderHash) const receiver = exec.instantiate(receiverCont.idx, 0, new Uint8Array([0])) const sender = exec.instantiate(senderCont.idx, 0, new Uint8Array([0])) exec.call(sender.idx, ...senderArgs.method('Sender', 'm1', [ref(receiver.idx), 1000])) + exec.lockJig(receiver.idx, userAddr) + exec.lockJig(sender.idx, userAddr) const result = exec.finalize() - expect(result.hydrosUsed).to.within(30, 40) + expect(result.hydrosUsed).to.within(35, 45) }) }); - it('adds hydros based on each container created', () => { - const {exec} = fundedExec() + it('adds hydros based on each container created', async () => { + const {exec} = await fundedExec() exec.import(modIdFor('flock')) exec.import(modIdFor('sheep')) const result = exec.finalize() expect(result.hydrosUsed).to.eql(6) }) - it('adds hydros based on each signer', () => { + it('adds hydros based on each signer', async () => { const key1 = PrivKey.fromRandom() const key2 = PrivKey.fromRandom() const key3 = PrivKey.fromRandom() - const {exec} = fundedExec([key1, key2, key3]) + const {exec} = await fundedExec([key1, key2, key3]) exec.sign(new Uint8Array(), new Uint8Array) exec.sign(new Uint8Array(), new Uint8Array) exec.sign(new Uint8Array(), new Uint8Array) @@ -915,16 +954,20 @@ describe('execute txs', () => { expect(result.hydrosUsed).to.eql(7) }) - it('adds hydros based on each load by origin', () => { - const {exec: exec1} = fundedExec([]) + it('adds hydros based on each load by origin', async () => { + const {exec: exec1} = await fundedExec([]) const pkgStmt = exec1.import(modIdFor('flock')) + const jig1Stmt = exec1.instantiate(pkgStmt.idx, 0, new Uint8Array([0])) exec1.instantiate(pkgStmt.idx, 0, new Uint8Array([0])) exec1.instantiate(pkgStmt.idx, 0, new Uint8Array([0])) - exec1.instantiate(pkgStmt.idx, 0, new Uint8Array([0])) + exec1.lockJig(jig1Stmt.idx, userAddr) + exec1.lockJig(jig1Stmt.idx + 1, userAddr) + exec1.lockJig(jig1Stmt.idx + 2, userAddr) + const result1 = exec1.finalize(); - storage.persistExecResult(result1) + await storage.persistExecResult(result1) - const {exec: exec2} = fundedExec([]) + const {exec: exec2} = await fundedExec([]) exec2.loadByOrigin(result1.outputs[1].origin.toBytes()) exec2.loadByOrigin(result1.outputs[2].origin.toBytes()) @@ -936,23 +979,26 @@ describe('execute txs', () => { it('count hydros for compile', async () => { const opts = ExecOpts.default() opts.deployHydroCost = 500n - const {exec} = fundedExec([], opts) + const {exec} = await fundedExec([], opts) await exec.deploy(['index.ts'], new Map([['index.ts', 'export class A extends Jig {}']])) const res = exec.finalize() expect(res.hydrosUsed).to.eql(504) }) it('count hydros for new ouputs', async () => { - const {exec: exec1} = fundedExec([]) + const {exec: exec1} = await fundedExec([]) const pkgStmt = exec1.import(modIdFor('flock')) + const jig1 = exec1.instantiate(pkgStmt.idx, 0, new Uint8Array([0])) exec1.instantiate(pkgStmt.idx, 0, new Uint8Array([0])) exec1.instantiate(pkgStmt.idx, 0, new Uint8Array([0])) - exec1.instantiate(pkgStmt.idx, 0, new Uint8Array([0])) - const result1 = exec1.finalize(); + exec1.lockJig(jig1.idx, userAddr) + exec1.lockJig(jig1.idx + 1, userAddr) + exec1.lockJig(jig1.idx + 2, userAddr) + const result1 = exec1.finalize() expect(result1.hydrosUsed).to.eql(8) - storage.persistExecResult(result1) + await storage.persistExecResult(result1) - const {exec: exec2} = fundedExec([]) + const {exec: exec2} = await fundedExec([]) exec2.load(result1.outputs[1].hash) exec2.load(result1.outputs[2].hash) @@ -961,22 +1007,31 @@ describe('execute txs', () => { expect(result2.hydrosUsed).to.eql(5) }) - it('passes Aarons test', () => { + it('passes Aarons test', async () => { const args = new ArgsBuilder(abiFor('human')) - const {exec: exec1} = fundedExec() + const {exec: exec1} = await fundedExec() const pkgStmt = exec1.import(modIdFor('human')) const jig1Stmt = exec1.instantiate(pkgStmt.idx, ...args.constr('Human', ['name1', []])) const jig2Stmt = exec1.instantiate(pkgStmt.idx, ...args.constr('Human', ['name2', []])) + exec1.lockJig(jig1Stmt.idx, userAddr) + exec1.lockJig(jig2Stmt.idx, userAddr) const res1 = exec1.finalize(); - storage.persistExecResult(res1) + await storage.persistExecResult(res1) - const {exec: exec2} = fundedExec() + const {exec: exec2} = await fundedExec([userPriv]) const p1 = exec2.load(res1.outputs[1].hash) const p2 = exec2.load(res1.outputs[2].hash) exec2.call(p1.idx, ...args.method('Human', 'marry', [ref(p2.idx)] )) exec2.call(p1.idx, ...args.method('Human', 'child', ['bob'] )) + }) + + it('fails when a jig was not locked at the end of the tx', async () => { + const {exec: exec1} = await fundedExec([]) + const pkgStmt = exec1.import(modIdFor('flock')) + exec1.instantiate(pkgStmt.idx, 0, new Uint8Array([0])) + expect(() => exec1.finalize()).to.throw(PermissionError) }) // it('receives right amount from properties of foreign jigs', () => { diff --git a/packages/vm/test/util.ts b/packages/vm/test/util.ts index 4e1ae43d..9c83728e 100644 --- a/packages/vm/test/util.ts +++ b/packages/vm/test/util.ts @@ -1,7 +1,7 @@ -import {Storage, VM} from "../src/index.js"; +import {MemStorage, VM} from "../src/index.js"; import {base16, BCS, Output, PrivKey} from "@aldea/core"; import {TxExecution} from "../src/tx-execution.js"; -import {StorageTxContext} from "../src/tx-context/storage-tx-context.js"; +import {StorageTxContext} from "../src/exec-context/storage-tx-context.js"; import fs from "fs"; import {fileURLToPath} from "url"; import {compile} from "@aldea/compiler"; @@ -9,11 +9,11 @@ import {randomBytes} from "@aldea/core/support/util"; import {Abi, AbiQuery} from "@aldea/core/abi"; import {AbiAccess} from "../src/memory/abi-helpers/abi-access.js"; import {expect} from "chai"; -import {ExecOpts} from "../src/export-opts.js"; +import {ExecOpts} from "../src/exec-opts.js"; const __dir = fileURLToPath(new URL('.', import.meta.url)); -export const fundedExecFactoryFactory = (lazyStorage: () => Storage, lazyVm: () => VM) => (privKeys: PrivKey[] = [], opts: ExecOpts = ExecOpts.default()) => { +export const fundedExecFactoryFactory = (lazyStorage: () => MemStorage, lazyVm: () => VM) => async (privKeys: PrivKey[] = [], opts: ExecOpts = ExecOpts.default()) => { const storage = lazyStorage() const vm = lazyVm() const txHash = randomBytes(32) @@ -23,9 +23,9 @@ export const fundedExecFactoryFactory = (lazyStorage: () => Storage, lazyVm: () const context = new StorageTxContext(txHash, pubKeys, storage, vm) const exec = new TxExecution(context, opts) - const output = vm.mint(pubKeys[0].toAddress(), 100, new Uint8Array(32).fill(1)) + const output = await vm.mint(pubKeys[0].toAddress(), 100) - const stmt = exec.load(output.hash) + const stmt = exec.load(output.hash) exec.fund(stmt.idx) return { exec, txHash } } @@ -41,7 +41,7 @@ export function addPreCompiled (vm: VM, src: string ): Uint8Array { export function buildVm(sources: string[]) { const moduleIds = new Map() - const storage = new Storage() + const storage = new MemStorage() const vm = new VM(storage, compile) sources.forEach(src => {