From b8b76295d52791ce1e952c71a69dfc7619a18630 Mon Sep 17 00:00:00 2001 From: migue Date: Mon, 18 Dec 2023 15:45:46 -0300 Subject: [PATCH 01/17] Refactor code to adjust paths and update methods Adjusted the paths used to access 'well-known-abi-nodes.js' after it was moved one folder up. In addition, several methods and const variables have been updated to improve conciseness and readability. An example is replacing AbiType.fromName('ArrayBuffer') with AbiType.buffer() for clarity. --- packages/vm/src/index.ts | 1 + packages/vm/src/jig-init-params.ts | 2 +- .../vm/src/memory/abi-helpers/abi-access.ts | 2 +- .../vm/src/memory/abi-helpers/abi-class.ts | 2 +- .../memory/abi-helpers/abi-imported-proxy.ts | 2 -- .../vm/src/memory/abi-helpers/abi-method.ts | 2 +- .../vm/src/memory/abi-helpers/abi-type.ts | 6 +++++- .../vm/src/memory/abi-helpers/proxy-def.ts | 2 +- packages/vm/src/memory/lower-value.ts | 4 ++-- packages/vm/src/memory/value-lifter.ts | 2 +- packages/vm/src/tx-execution.ts | 6 +++--- packages/vm/src/vm.ts | 19 +++++++++++-------- packages/vm/src/wasm-container.ts | 6 +++--- .../src/{memory => }/well-known-abi-nodes.ts | 13 ++----------- packages/vm/test/coin.spec.ts | 2 +- packages/vm/test/new-memory-lift.spec.ts | 2 +- packages/vm/test/new-memory-lower.spec.ts | 6 +++--- packages/vm/test/tx-execution.spec.ts | 2 +- 18 files changed, 39 insertions(+), 42 deletions(-) rename packages/vm/src/{memory => }/well-known-abi-nodes.ts (89%) diff --git a/packages/vm/src/index.ts b/packages/vm/src/index.ts index 0cc99682..a3e49848 100644 --- a/packages/vm/src/index.ts +++ b/packages/vm/src/index.ts @@ -3,3 +3,4 @@ export * from './storage.js' export * from './calculate-package-id.js' export * from './tx-context/extended-tx.js' export * from './execution-result.js' +export {COIN_PKG_ID} from "./well-known-abi-nodes.js"; diff --git a/packages/vm/src/jig-init-params.ts b/packages/vm/src/jig-init-params.ts index 4913be5a..b4e514b0 100644 --- a/packages/vm/src/jig-init-params.ts +++ b/packages/vm/src/jig-init-params.ts @@ -1,7 +1,7 @@ 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"; diff --git a/packages/vm/src/memory/abi-helpers/abi-access.ts b/packages/vm/src/memory/abi-helpers/abi-access.ts index b507abfb..3dd48c07 100644 --- a/packages/vm/src/memory/abi-helpers/abi-access.ts +++ b/packages/vm/src/memory/abi-helpers/abi-access.ts @@ -9,7 +9,7 @@ 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"; 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/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/tx-execution.ts b/packages/vm/src/tx-execution.ts index 447792ea..b4c3d892 100644 --- a/packages/vm/src/tx-execution.ts +++ b/packages/vm/src/tx-execution.ts @@ -3,7 +3,7 @@ import {ExecutionError, IvariantBroken} 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"; @@ -546,7 +546,7 @@ class TxExecution { .low .lower( serializePointer(new Pointer(from.hash, abiClass.idx)), - AbiType.fromName('ArrayBuffer') + AbiType.buffer() ).toWasmArg(AbiType.u32()) } @@ -761,7 +761,7 @@ class TxExecution { throw new Error(`unknown vmCallerOutputVal key: ${key}`) } - return from.low.lower(buf.data, AbiType.fromName('ArrayBuffer')) + return from.low.lower(buf.data, AbiType.buffer()) } vmJigAuthCheck (from: WasmContainer, targetOriginPtr: WasmWord, check: AuthCheck): boolean { diff --git a/packages/vm/src/vm.ts b/packages/vm/src/vm.ts index b83251c8..3a927358 100644 --- a/packages/vm/src/vm.ts +++ b/packages/vm/src/vm.ts @@ -13,22 +13,25 @@ import {StorageTxContext} from "./tx-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 {COIN_PKG_ID} from "./well-known-abi-nodes.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; export class VM { diff --git a/packages/vm/src/wasm-container.ts b/packages/vm/src/wasm-container.ts index 29cb0c94..89037258 100644 --- a/packages/vm/src/wasm-container.ts +++ b/packages/vm/src/wasm-container.ts @@ -121,7 +121,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()}`) } @@ -159,12 +159,12 @@ export class WasmContainer { } 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() } 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() } 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/coin.spec.ts b/packages/vm/test/coin.spec.ts index 442161df..ea069c5c 100644 --- a/packages/vm/test/coin.spec.ts +++ b/packages/vm/test/coin.spec.ts @@ -9,7 +9,7 @@ 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 diff --git a/packages/vm/test/new-memory-lift.spec.ts b/packages/vm/test/new-memory-lift.spec.ts index ef781686..eb432ccd 100644 --- a/packages/vm/test/new-memory-lift.spec.ts +++ b/packages/vm/test/new-memory-lift.spec.ts @@ -6,7 +6,7 @@ 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"; diff --git a/packages/vm/test/new-memory-lower.spec.ts b/packages/vm/test/new-memory-lower.spec.ts index a5e83574..35b69f19 100644 --- a/packages/vm/test/new-memory-lower.spec.ts +++ b/packages/vm/test/new-memory-lower.spec.ts @@ -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"; @@ -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/tx-execution.spec.ts b/packages/vm/test/tx-execution.spec.ts index 0854fb3d..a8feb638 100644 --- a/packages/vm/test/tx-execution.spec.ts +++ b/packages/vm/test/tx-execution.spec.ts @@ -3,7 +3,7 @@ import {expect} from 'chai' import {base16, BCS, BufReader, LockType, Output, Pointer, PrivKey, PubKey, ref} 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"; From cfd3d37963de3eaa8b6044bf57d6b2d2e7a41ac0 Mon Sep 17 00:00:00 2001 From: migue Date: Mon, 18 Dec 2023 16:26:28 -0300 Subject: [PATCH 02/17] added jsdocs for vm --- packages/vm/src/vm.ts | 61 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/packages/vm/src/vm.ts b/packages/vm/src/vm.ts index 3a927358..aabe29a3 100644 --- a/packages/vm/src/vm.ts +++ b/packages/vm/src/vm.ts @@ -26,6 +26,7 @@ import { } from "@aldea/core/instructions"; import {ExecOpts} from "./export-opts.js"; import {COIN_PKG_ID} from "./well-known-abi-nodes.js"; +import {ExecutionError} from "./errors.js"; /** @@ -34,16 +35,36 @@ import {COIN_PKG_ID} from "./well-known-abi-nodes.js"; */ 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 compile: CompileFn; + /** + * Creates a new instance of the constructor. + * + * @param {Storage} storage - The storage object for retrieving storing data. + * @param {CompileFn} compile - The compile function to manaage deploys. + */ constructor (storage: Storage, 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()) @@ -96,7 +117,7 @@ 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() @@ -105,14 +126,14 @@ export class VM { 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) @@ -137,6 +158,18 @@ 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()) @@ -162,6 +195,16 @@ export class VM { return id } + /** + * 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. + */ mint (address: Address, amount: number = 1e6, locBuf?: Uint8Array): Output { const location = locBuf ? new Pointer(locBuf, 0) From e164350a523751c1a52d9aa902798296778d654f Mon Sep 17 00:00:00 2001 From: migue Date: Mon, 18 Dec 2023 16:54:34 -0300 Subject: [PATCH 03/17] Renamed stateByOutId to outputById. Added docs to ExecContext --- packages/vm/src/tx-context/exec-context.ts | 50 +++++++++++++++++-- .../vm/src/tx-context/storage-tx-context.ts | 6 +-- packages/vm/src/tx-execution.ts | 4 +- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/packages/vm/src/tx-context/exec-context.ts b/packages/vm/src/tx-context/exec-context.ts index 27e4e668..f40f93ea 100644 --- a/packages/vm/src/tx-context/exec-context.ts +++ b/packages/vm/src/tx-context/exec-context.ts @@ -2,19 +2,61 @@ import {Output, Pointer, PubKey} from "@aldea/core"; import {WasmContainer} from "../wasm-container.js"; import {PkgData} from "../storage.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 - stateByOutputId (id: Uint8Array): Output - + /** + * 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 - txId (): string - + /** + * 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/storage-tx-context.ts b/packages/vm/src/tx-context/storage-tx-context.ts index 6eb9686e..869c35c5 100644 --- a/packages/vm/src/tx-context/storage-tx-context.ts +++ b/packages/vm/src/tx-context/storage-tx-context.ts @@ -22,7 +22,7 @@ export class StorageTxContext implements ExecContext { return this._txHash } - stateByOutputId (id: Uint8Array): Output { + outputById (id: Uint8Array): Output { return this.storage.byOutputId(id).orElse(() => { throw new ExecutionError(`output not present in utxo set: ${base16.encode(id)}`) }) @@ -42,10 +42,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/tx-execution.ts b/packages/vm/src/tx-execution.ts index b4c3d892..b14eac0f 100644 --- a/packages/vm/src/tx-execution.ts +++ b/packages/vm/src/tx-execution.ts @@ -130,7 +130,7 @@ class TxExecution { } finalize (): ExecutionResult { - const result = new ExecutionResult(this.execContext.txId()) + 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}`) } @@ -228,7 +228,7 @@ class TxExecution { } 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); From 9e035073485d63552ae94e6cda913d1871a8d69b Mon Sep 17 00:00:00 2001 From: migue Date: Mon, 18 Dec 2023 17:09:36 -0300 Subject: [PATCH 04/17] failing when jigs were not locked --- packages/vm/src/locks/lock.ts | 4 +++ packages/vm/src/locks/open-lock.ts | 4 +++ packages/vm/src/tx-execution.ts | 14 ++++----- packages/vm/test/caller-api.spec.ts | 13 +++++++- packages/vm/test/tx-execution.spec.ts | 44 ++++++++++++++++++++++----- 5 files changed, 63 insertions(+), 16 deletions(-) diff --git a/packages/vm/src/locks/lock.ts b/packages/vm/src/locks/lock.ts index c39c84be..a092f49b 100644 --- a/packages/vm/src/locks/lock.ts +++ b/packages/vm/src/locks/lock.ts @@ -18,6 +18,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..874d2698 100644 --- a/packages/vm/src/locks/open-lock.ts +++ b/packages/vm/src/locks/open-lock.ts @@ -18,4 +18,8 @@ export class OpenLock extends Lock { canBeChanged (_param: TxExecution): boolean { return true; } + + isOpen (): boolean { + return true + } } diff --git a/packages/vm/src/tx-execution.ts b/packages/vm/src/tx-execution.ts index b14eac0f..a3dae174 100644 --- a/packages/vm/src/tx-execution.ts +++ b/packages/vm/src/tx-execution.ts @@ -1,5 +1,5 @@ import {ContainerRef, JigRef} from "./jig-ref.js" -import {ExecutionError, IvariantBroken} from "./errors.js" +import {ExecutionError, IvariantBroken, 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'; @@ -134,12 +134,12 @@ class TxExecution { 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.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) diff --git a/packages/vm/test/caller-api.spec.ts b/packages/vm/test/caller-api.spec.ts index 33e0e47a..11831dcc 100644 --- a/packages/vm/test/caller-api.spec.ts +++ b/packages/vm/test/caller-api.spec.ts @@ -3,7 +3,7 @@ import { 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'; @@ -13,6 +13,9 @@ describe('execute txs', () => { let modIdFor: (key: string) => Uint8Array + const userKey = PrivKey.fromRandom() + const userAddr = userKey.toPubKey().toAddress() + let args: ArgsBuilder beforeEach(() => { const data = buildVm(['caller-test-code']) @@ -33,6 +36,9 @@ describe('execute txs', () => { 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]) @@ -45,6 +51,8 @@ describe('execute txs', () => { 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]) @@ -56,6 +64,7 @@ describe('execute txs', () => { 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]) @@ -71,6 +80,8 @@ describe('execute txs', () => { 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/tx-execution.spec.ts b/packages/vm/test/tx-execution.spec.ts index a8feb638..c9c1b401 100644 --- a/packages/vm/test/tx-execution.spec.ts +++ b/packages/vm/test/tx-execution.spec.ts @@ -533,8 +533,11 @@ describe('execute txs', () => { }) it('when call an external constructor a new jig is created a right proxy gets assigned', () => { - const {exec, shepherd} = shepherdExec([userPriv]) + const {exec, flock, shepherd} = 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) @@ -632,7 +635,8 @@ describe('execute txs', () => { const { exec } = 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])) }) @@ -641,7 +645,8 @@ describe('execute txs', () => { const { exec } = 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])) }) @@ -654,6 +659,8 @@ describe('execute txs', () => { 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])) @@ -699,6 +706,7 @@ describe('execute txs', () => { 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) @@ -714,6 +722,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) @@ -729,6 +738,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) @@ -890,8 +900,10 @@ describe('execute txs', () => { 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) }) }); @@ -918,9 +930,13 @@ describe('execute txs', () => { it('adds hydros based on each load by origin', () => { const {exec: exec1} = 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) @@ -945,10 +961,13 @@ describe('execute txs', () => { it('count hydros for new ouputs', async () => { const {exec: exec1} = 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) @@ -968,15 +987,24 @@ describe('execute txs', () => { 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) - const {exec: exec2} = fundedExec() + const {exec: exec2} = 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', () => { + const {exec: exec1} = 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', () => { From fce6ce85cfa1b3da9bb3826ac13d0b27cd572fdb Mon Sep 17 00:00:00 2001 From: migue Date: Mon, 18 Dec 2023 17:12:06 -0300 Subject: [PATCH 05/17] moved metering classes to its own files --- packages/vm/src/metering/discret-counter.ts | 46 +++++++++++++ packages/vm/src/metering/measurements.ts | 32 +++++++++ packages/vm/src/tx-execution.ts | 76 +-------------------- 3 files changed, 79 insertions(+), 75 deletions(-) create mode 100644 packages/vm/src/metering/discret-counter.ts create mode 100644 packages/vm/src/metering/measurements.ts diff --git a/packages/vm/src/metering/discret-counter.ts b/packages/vm/src/metering/discret-counter.ts new file mode 100644 index 00000000..0b67c09d --- /dev/null +++ b/packages/vm/src/metering/discret-counter.ts @@ -0,0 +1,46 @@ +import {ExecutionError} from "../errors.js"; + +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) + } +} diff --git a/packages/vm/src/metering/measurements.ts b/packages/vm/src/metering/measurements.ts new file mode 100644 index 00000000..2240d044 --- /dev/null +++ b/packages/vm/src/metering/measurements.ts @@ -0,0 +1,32 @@ +import {DiscretCounter} from "./discret-counter.js"; +import {ExecOpts} from "../export-opts.js"; + +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(); + } +} diff --git a/packages/vm/src/tx-execution.ts b/packages/vm/src/tx-execution.ts index a3dae174..37c4c0ea 100644 --- a/packages/vm/src/tx-execution.ts +++ b/packages/vm/src/tx-execution.ts @@ -22,84 +22,10 @@ import {ArgsTranslator} from "./args-translator.js"; import {CodeKind} from "@aldea/core/abi"; import {AbiArg} from "./memory/abi-helpers/abi-arg.js"; import {ExecOpts} from "./export-opts.js"; +import {Measurements} from "./metering/measurements.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 TxExecution { execContext: ExecContext; From a20e0dc68960504fabacbe7cf345d222441b2829 Mon Sep 17 00:00:00 2001 From: migue Date: Tue, 19 Dec 2023 09:37:25 -0300 Subject: [PATCH 06/17] added lots of docs to tx execution --- packages/vm/src/tx-execution.ts | 530 ++++++++++++++++++++++++-------- 1 file changed, 402 insertions(+), 128 deletions(-) diff --git a/packages/vm/src/tx-execution.ts b/packages/vm/src/tx-execution.ts index 37c4c0ea..7f10fad3 100644 --- a/packages/vm/src/tx-execution.ts +++ b/packages/vm/src/tx-execution.ts @@ -27,19 +27,63 @@ import {Measurements} from "./metering/measurements.js"; export const MIN_FUND_AMOUNT = 100 +/** + * 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 = [] @@ -55,104 +99,30 @@ class TxExecution { this.measurements.numSigs.add(BigInt(this.execContext.signers().length)) } - 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 - } + // ------- + // 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.outputById(outputId) const jigRef = this.hydrate(output) @@ -162,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) @@ -172,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() @@ -191,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); @@ -199,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() @@ -215,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 @@ -253,6 +434,17 @@ 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) @@ -261,7 +453,19 @@ class TxExecution { 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) => { @@ -269,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() @@ -284,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) { @@ -296,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) @@ -318,23 +545,15 @@ class TxExecution { .expect(new IvariantBroken('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) { @@ -348,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) { @@ -371,7 +606,17 @@ class TxExecution { } } - getJigData (p: Pointer): Option { + /** + * 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. + */ + private getJigData (p: Pointer): Option { const existing = this.jigs.find(j => j.origin.equals(p)) if (existing) { return Option.some({ @@ -393,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)) @@ -402,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() @@ -416,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]) } From cfd6f436c07e4fc02c2f30f07cc3143c676732a3 Mon Sep 17 00:00:00 2001 From: migue Date: Tue, 19 Dec 2023 10:59:51 -0300 Subject: [PATCH 07/17] Added documentation for tx execution --- packages/vm/src/errors.ts | 3 +- packages/vm/src/tx-execution.ts | 165 +++++++++++++++++++++++++++++--- 2 files changed, 154 insertions(+), 14 deletions(-) diff --git a/packages/vm/src/errors.ts b/packages/vm/src/errors.ts index adf23866..41e83504 100644 --- a/packages/vm/src/errors.ts +++ b/packages/vm/src/errors.ts @@ -1,5 +1,4 @@ export class PermissionError extends Error {} export class ExecutionError extends Error {} - -export class IvariantBroken extends Error {} +export class InvariantBroken extends Error {} diff --git a/packages/vm/src/tx-execution.ts b/packages/vm/src/tx-execution.ts index 7f10fad3..159c5494 100644 --- a/packages/vm/src/tx-execution.ts +++ b/packages/vm/src/tx-execution.ts @@ -1,5 +1,5 @@ import {ContainerRef, JigRef} from "./jig-ref.js" -import {ExecutionError, IvariantBroken, PermissionError} 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'; @@ -542,7 +542,7 @@ 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')) } @@ -616,7 +616,7 @@ class TxExecution { * @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. */ - private getJigData (p: Pointer): Option { + getJigData (p: Pointer): Option { const existing = this.jigs.find(j => j.origin.equals(p)) if (existing) { return Option.some({ @@ -701,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() @@ -719,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) @@ -750,6 +777,16 @@ class TxExecution { ).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) @@ -762,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) @@ -776,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) @@ -787,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)) @@ -816,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) @@ -834,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( @@ -847,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) @@ -860,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) @@ -883,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( @@ -896,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()) { @@ -924,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) @@ -940,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) @@ -958,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.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) @@ -977,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) } From 9e661b6c38501c82a85cddfba96efa5d1fb5a677 Mon Sep 17 00:00:00 2001 From: migue Date: Tue, 19 Dec 2023 11:21:18 -0300 Subject: [PATCH 08/17] Added docs to wasm word --- packages/vm/src/wasm-word.ts | 142 +++++++++++++++++++++++++++-------- 1 file changed, 111 insertions(+), 31 deletions(-) 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) } From 811fb4261b9c2e6eab364c08ef53e10812cc85d3 Mon Sep 17 00:00:00 2001 From: migue Date: Tue, 19 Dec 2023 11:32:09 -0300 Subject: [PATCH 09/17] docs for wasm container --- packages/vm/src/wasm-container.ts | 81 +++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/packages/vm/src/wasm-container.ts b/packages/vm/src/wasm-container.ts index 89037258..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, @@ -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.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.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) } From 7eba6c58d956df98964f502f4b0dc58a91947d1a Mon Sep 17 00:00:00 2001 From: migue Date: Tue, 19 Dec 2023 15:55:53 -0300 Subject: [PATCH 10/17] refactors to allow test everything with up to date utxoset --- packages/mocknet/src/server.ts | 4 +- packages/vm/src/storage.ts | 138 ++++++++---------- packages/vm/src/storage/generic-storage.ts | 0 .../vm/src/tx-context/ex-tx-exec-context.ts | 72 ++++++++- .../vm/src/tx-context/storage-tx-context.ts | 4 +- packages/vm/src/vm.ts | 19 +-- packages/vm/test/tx-execution.spec.ts | 35 ++++- packages/vm/test/util.ts | 2 +- 8 files changed, 178 insertions(+), 96 deletions(-) create mode 100644 packages/vm/src/storage/generic-storage.ts 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/src/storage.ts b/packages/vm/src/storage.ts index 3cdec880..73508bc4 100644 --- a/packages/vm/src/storage.ts +++ b/packages/vm/src/storage.ts @@ -45,20 +45,18 @@ export class PkgData { } export class Storage { - private utxosByOid: Map // output_id -> state. Only utxos + 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 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.utxosByOutputId = new Map() this.tips = new Map() - this.origins = new Map() this.txs = new Map() this.execResults = new Map() this.packages = new Map() @@ -68,90 +66,75 @@ export class Storage { } - persistTx(tx: Tx) { + /** + * Persists a transaction. + * + * @param {Tx} tx - The transaction to persist. + * + * @return {void} + */ + async persistTx(tx: Tx): Promise { this.txs.set(tx.id, tx) } - persistExecResult(txExecution: ExecutionResult) { + async persistExecResult(txExecution: ExecutionResult): Promise { 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 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 (!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) - } + 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) } - 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 address = new Address(output.lock.data); + const previousByAddr = this.utxosForAddress(address) + previousByAddr.push(output) + this.utxosByAddress.set(address.toString(), previousByAddr) } - 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) - } + const serializedLock = output.lock.toHex(); + const previousByLock = this.utxosForLock(serializedLock) + previousByLock.push(output) + this.utxosByAddress.set(serializedLock, previousByLock) - getJigStateByOutputId (outputId: Uint8Array): Option { - const state = this.utxosByOid.get(base16.encode(outputId)) - return Option.fromNullable(state) + this.tips.set(originStr, currentOutputId) + this.historicalUtxos.set(currentOutputId, output) } - tipFor(origin: Pointer): Uint8Array { + /** + * 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()); - if (!tip) throw new Error('not found') - return base16.decode(tip) + return Option.fromNullable(tip) } getTx(txid: string): Option { @@ -171,10 +154,6 @@ export class Storage { 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) @@ -190,12 +169,21 @@ export class Storage { .orDefault([]) } - byOutputId(id: Uint8Array): Option { - return this.getJigStateByOutputId(id); + outputByHash(hash: Uint8Array): Option { + const id = base16.encode(hash) + return this.outputById(id) } - byOrigin(origin: Pointer): Option { - return this.getJigStateByOrigin(origin); + 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 { diff --git a/packages/vm/src/storage/generic-storage.ts b/packages/vm/src/storage/generic-storage.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/vm/src/tx-context/ex-tx-exec-context.ts b/packages/vm/src/tx-context/ex-tx-exec-context.ts index fc670613..831a245e 100644 --- a/packages/vm/src/tx-context/ex-tx-exec-context.ts +++ b/packages/vm/src/tx-context/ex-tx-exec-context.ts @@ -29,7 +29,74 @@ // } // } // -// export class ExTxExecContext implements ExecContext { +import {ExecContext} from "./exec-context.js"; +import {PkgData} from "../storage.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 {calculatePackageId} from "../calculate-package-id.js"; +import {Buffer} from "buffer"; +import {Option} from "../support/option.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 = calculatePackageId(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/tx-context/storage-tx-context.ts b/packages/vm/src/tx-context/storage-tx-context.ts index 869c35c5..68e40513 100644 --- a/packages/vm/src/tx-context/storage-tx-context.ts +++ b/packages/vm/src/tx-context/storage-tx-context.ts @@ -23,13 +23,13 @@ export class StorageTxContext implements ExecContext { } outputById (id: Uint8Array): Output { - return this.storage.byOutputId(id).orElse(() => { + 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 { diff --git a/packages/vm/src/vm.ts b/packages/vm/src/vm.ts index aabe29a3..8ec71d09 100644 --- a/packages/vm/src/vm.ts +++ b/packages/vm/src/vm.ts @@ -1,6 +1,6 @@ import {PkgData, Storage} from "./storage.js"; import {CompilerResult, PackageParser, writeDependency} from '@aldea/compiler' -import {abiFromBin, Address, BCS, OpCode, Output, Pointer, Tx, util} from "@aldea/core"; +import {abiFromBin, Address, base16, BCS, OpCode, Output, Pointer, Tx, util} from "@aldea/core"; import {calculatePackageId} from "./calculate-package-id.js"; import {Buffer} from "buffer"; import {data as wasm} from './builtins/coin.wasm.js' @@ -121,8 +121,8 @@ export class VM { } } const result = currentExec.finalize() - this.storage.persistTx(tx) - this.storage.persistExecResult(result) + await this.storage.persistTx(tx) + await this.storage.persistExecResult(result) return result } @@ -174,25 +174,26 @@ export class VM { 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 + 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 } /** diff --git a/packages/vm/test/tx-execution.spec.ts b/packages/vm/test/tx-execution.spec.ts index c9c1b401..0f3fd40d 100644 --- a/packages/vm/test/tx-execution.spec.ts +++ b/packages/vm/test/tx-execution.spec.ts @@ -1,6 +1,6 @@ -import {Storage, VM} from '../src/index.js' +import {ExtendedTx, Storage, 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/well-known-abi-nodes.js"; @@ -11,6 +11,9 @@ import {ExecutionResult} from "../src/index.js"; import {StorageTxContext} from "../src/tx-context/storage-tx-context.js"; import {randomBytes} from "@aldea/core/support/util"; import {ExecOpts} from "../src/export-opts.js"; +import {ExTxExecContext} from "../src/tx-context/ex-tx-exec-context.js"; +import {SignInstruction} from "@aldea/core/instructions"; +import {compile} from "@aldea/compiler"; describe('execute txs', () => { let storage: Storage @@ -275,7 +278,19 @@ describe('execute txs', () => { }) 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, diff --git a/packages/vm/test/util.ts b/packages/vm/test/util.ts index 4e1ae43d..fa7dd4c1 100644 --- a/packages/vm/test/util.ts +++ b/packages/vm/test/util.ts @@ -23,7 +23,7 @@ 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 = vm.mint(pubKeys[0].toAddress(), 100) const stmt = exec.load(output.hash) exec.fund(stmt.idx) From c4f582f1b20824b3f0dc21fdcfa221fcf0d1bdc3 Mon Sep 17 00:00:00 2001 From: migue Date: Tue, 19 Dec 2023 16:17:20 -0300 Subject: [PATCH 11/17] lots of refactos in storage --- .../ex-tx-exec-context.ts | 2 +- .../exec-context.ts | 3 +- .../extended-tx.ts | 0 .../storage-tx-context.ts | 11 +- packages/vm/src/execution-result.ts | 3 +- packages/vm/src/index.ts | 6 +- packages/vm/src/storage/generic-storage.ts | 77 +++++ .../{storage.ts => storage/mem-storage.ts} | 83 ++---- packages/vm/src/storage/pkg-data.ts | 42 +++ packages/vm/src/tx-execution.ts | 4 +- packages/vm/src/vm.ts | 15 +- packages/vm/test/caller-api.spec.ts | 20 +- packages/vm/test/coin.spec.ts | 10 +- packages/vm/test/exec-from-inputs.spec.ts | 2 +- packages/vm/test/new-memory-lift.spec.ts | 4 +- packages/vm/test/new-memory-lower.spec.ts | 4 +- packages/vm/test/specific-scenarios.spec.ts | 20 +- packages/vm/test/tx-execution.spec.ts | 282 +++++++++--------- packages/vm/test/util.ts | 12 +- 19 files changed, 345 insertions(+), 255 deletions(-) rename packages/vm/src/{tx-context => exec-context}/ex-tx-exec-context.ts (98%) rename packages/vm/src/{tx-context => exec-context}/exec-context.ts (97%) rename packages/vm/src/{tx-context => exec-context}/extended-tx.ts (100%) rename packages/vm/src/{tx-context => exec-context}/storage-tx-context.ts (76%) rename packages/vm/src/{storage.ts => storage/mem-storage.ts} (69%) create mode 100644 packages/vm/src/storage/pkg-data.ts 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 98% 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 831a245e..7e6d5043 100644 --- a/packages/vm/src/tx-context/ex-tx-exec-context.ts +++ b/packages/vm/src/exec-context/ex-tx-exec-context.ts @@ -30,7 +30,6 @@ // } // import {ExecContext} from "./exec-context.js"; -import {PkgData} from "../storage.js"; import {abiFromBin, base16, Output, Pointer, PubKey} from "@aldea/core"; import {WasmContainer} from "../wasm-container.js"; import {ExtendedTx} from "./extended-tx.js"; @@ -39,6 +38,7 @@ import {ExecutionError} from "../errors.js"; import {calculatePackageId} from "../calculate-package-id.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 diff --git a/packages/vm/src/tx-context/exec-context.ts b/packages/vm/src/exec-context/exec-context.ts similarity index 97% rename from packages/vm/src/tx-context/exec-context.ts rename to packages/vm/src/exec-context/exec-context.ts index f40f93ea..c9fa6c41 100644 --- a/packages/vm/src/tx-context/exec-context.ts +++ b/packages/vm/src/exec-context/exec-context.ts @@ -1,6 +1,7 @@ import {Output, Pointer, PubKey} from "@aldea/core"; import {WasmContainer} from "../wasm-container.js"; -import {PkgData} from "../storage.js"; + +import {PkgData} from "../storage/pkg-data.js"; /** * This interface represents everything that is needed for a VM 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 76% rename from packages/vm/src/tx-context/storage-tx-context.ts rename to packages/vm/src/exec-context/storage-tx-context.ts index 68e40513..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 diff --git a/packages/vm/src/execution-result.ts b/packages/vm/src/execution-result.ts index f0048e17..4d403b49 100644 --- a/packages/vm/src/execution-result.ts +++ b/packages/vm/src/execution-result.ts @@ -4,7 +4,8 @@ import {Abi} from "@aldea/core/abi"; import {ExecutionError} from "./errors.js"; import {calculatePackageId} from "./calculate-package-id.js"; import {Option} from "./support/option.js"; -import {PkgData} from "./storage.js"; + +import {PkgData} from "./storage/pkg-data.js"; export class PackageDeploy { sources: Map diff --git a/packages/vm/src/index.ts b/packages/vm/src/index.ts index a3e49848..6915c62f 100644 --- a/packages/vm/src/index.ts +++ b/packages/vm/src/index.ts @@ -1,6 +1,8 @@ export * from './vm.js' -export * from './storage.js' +export * from './storage/mem-storage.js' export * from './calculate-package-id.js' -export * from './tx-context/extended-tx.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/storage/generic-storage.ts b/packages/vm/src/storage/generic-storage.ts index e69de29b..a681484b 100644 --- a/packages/vm/src/storage/generic-storage.ts +++ 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.ts b/packages/vm/src/storage/mem-storage.ts similarity index 69% rename from packages/vm/src/storage.ts rename to packages/vm/src/storage/mem-storage.ts index 73508bc4..c20f280a 100644 --- a/packages/vm/src/storage.ts +++ b/packages/vm/src/storage/mem-storage.ts @@ -1,50 +1,11 @@ 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 - ) - } -} +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"; -export class 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 @@ -73,17 +34,17 @@ export class Storage { * * @return {void} */ - async persistTx(tx: Tx): Promise { + async persistTx (tx: Tx): Promise { this.txs.set(tx.id, tx) } - async persistExecResult(txExecution: ExecutionResult): Promise { + async persistExecResult (txExecution: ExecutionResult): Promise { this.execResults.set(txExecution.txId, txExecution) - txExecution.outputs.forEach((state) => this.addUtxo(state)) + await Promise.all(txExecution.outputs.map((state) => this.addUtxo(state))) txExecution.deploys.forEach(pkgDeploy => this.addPackage(pkgDeploy.hash, PkgData.fromPackageDeploy(pkgDeploy))) } - addUtxo(output: Output) { + async addUtxo (output: Output): Promise { const currentOutputId = output.id; const originStr = output.origin.toString(); const origin = Pointer.fromString(originStr) @@ -132,25 +93,25 @@ export class Storage { * @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 { + tipFor (origin: Pointer): Option { const tip = this.tips.get(origin.toString()); return Option.fromNullable(tip) } - getTx(txid: string): Option { + getTx (txid: string): Option { return Option.fromNullable(this.txs.get(txid)) } - getExecResult(txid: string): Option { + getExecResult (txid: string): Option { return Option.fromNullable(this.execResults.get(txid)) } - addPackage(id: Uint8Array, pkgData: PkgData): void { + addPackage (id: Uint8Array, pkgData: PkgData): void { this.packages.set(base16.encode(id), pkgData) } getPkg (id: string): Option { - const pkg = this.packages.get(id) + const pkg = this.packages.get(id) return Option.fromNullable(pkg) } @@ -159,35 +120,35 @@ export class Storage { return Option.fromNullable(state) } - utxosForAddress(userAddr: Address): Output[] { + utxosForAddress (userAddr: Address): Output[] { return Option.fromNullable(this.utxosByAddress.get(userAddr.toString())) .orDefault([]) } - utxosForLock(lockHex: string): Output[] { + utxosForLock (lockHex: string): Output[] { return Option.fromNullable(this.utxosByLock.get(lockHex)) .orDefault([]) } - outputByHash(hash: Uint8Array): Option { + outputByHash (hash: Uint8Array): Option { const id = base16.encode(hash) return this.outputById(id) } - outputById(id: string): Option { + outputById (id: string): Option { const state = this.utxosByOutputId.get(id) return Option.fromNullable(state) } - outputByOrigin(origin: Pointer): Option { + 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 => { + 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..4d8b9da1 --- /dev/null +++ b/packages/vm/src/storage/pkg-data.ts @@ -0,0 +1,42 @@ +import {Abi} from "@aldea/core/abi"; +import {PackageDeploy} from "../execution-result.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 + ) + } +} diff --git a/packages/vm/src/tx-execution.ts b/packages/vm/src/tx-execution.ts index 159c5494..6aa4157b 100644 --- a/packages/vm/src/tx-execution.ts +++ b/packages/vm/src/tx-execution.ts @@ -6,8 +6,7 @@ import {Address, base16, BufReader, BufWriter, Lock as CoreLock, LockType, Outpu 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"; @@ -23,6 +22,7 @@ import {CodeKind} from "@aldea/core/abi"; import {AbiArg} from "./memory/abi-helpers/abi-arg.js"; import {ExecOpts} from "./export-opts.js"; import {Measurements} from "./metering/measurements.js"; +import {PkgData} from "./storage/pkg-data.js"; export const MIN_FUND_AMOUNT = 100 diff --git a/packages/vm/src/vm.ts b/packages/vm/src/vm.ts index 8ec71d09..d3136b16 100644 --- a/packages/vm/src/vm.ts +++ b/packages/vm/src/vm.ts @@ -1,4 +1,4 @@ -import {PkgData, Storage} from "./storage.js"; +import {MemStorage} from "./storage/mem-storage.js"; import {CompilerResult, PackageParser, writeDependency} from '@aldea/compiler' import {abiFromBin, Address, base16, BCS, OpCode, Output, Pointer, Tx, util} from "@aldea/core"; import {calculatePackageId} from "./calculate-package-id.js"; @@ -9,7 +9,7 @@ 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, @@ -27,6 +27,7 @@ import { import {ExecOpts} from "./export-opts.js"; import {COIN_PKG_ID} from "./well-known-abi-nodes.js"; import {ExecutionError} from "./errors.js"; +import {PkgData} from "./storage/pkg-data.js"; /** @@ -42,16 +43,16 @@ export type CompileFn = (entry: string[], src: Map, deps: Map { const location = locBuf ? new Pointer(locBuf, 0) : new Pointer(util.randomBytes(32), 0); @@ -218,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/test/caller-api.spec.ts b/packages/vm/test/caller-api.spec.ts index 11831dcc..a57b40d8 100644 --- a/packages/vm/test/caller-api.spec.ts +++ b/packages/vm/test/caller-api.spec.ts @@ -1,5 +1,5 @@ import { - Storage, + MemStorage, VM } from '../src/index.js' import {expect} from 'chai' @@ -8,7 +8,7 @@ import {base16, BufReader, PrivKey, ref} from "@aldea/core"; 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 @@ -30,8 +30,8 @@ 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', [])) @@ -45,8 +45,8 @@ describe('execute txs', () => { 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', [])) @@ -59,8 +59,8 @@ describe('execute txs', () => { 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', [])) @@ -74,8 +74,8 @@ 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', [])) diff --git a/packages/vm/test/coin.spec.ts b/packages/vm/test/coin.spec.ts index ea069c5c..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"; @@ -12,7 +12,7 @@ import { 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 eb432ccd..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"; @@ -12,7 +12,7 @@ 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 35b69f19..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"; @@ -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 diff --git a/packages/vm/test/specific-scenarios.spec.ts b/packages/vm/test/specific-scenarios.spec.ts index 9ec510c2..ce256d32 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 { 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 0f3fd40d..87752422 100644 --- a/packages/vm/test/tx-execution.spec.ts +++ b/packages/vm/test/tx-execution.spec.ts @@ -1,4 +1,4 @@ -import {ExtendedTx, 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, Tx} from "@aldea/core"; import {Abi} from '@aldea/core/abi'; @@ -8,15 +8,15 @@ 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 {ExTxExecContext} from "../src/tx-context/ex-tx-exec-context.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() @@ -64,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) @@ -78,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]) @@ -92,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])) @@ -104,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])) @@ -117,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', [])) @@ -136,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', [])) @@ -158,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', [])) @@ -174,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', [])) @@ -194,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) @@ -212,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', [])) @@ -234,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', [])) @@ -248,33 +248,33 @@ 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', () => { @@ -318,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', [])) @@ -334,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', [])) @@ -350,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) @@ -379,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', [])) @@ -389,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) @@ -398,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()) @@ -424,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', [])) @@ -436,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) @@ -450,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', [])) @@ -468,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 @@ -490,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', [])) @@ -526,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]]) @@ -538,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)) @@ -559,8 +559,8 @@ 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, flock, 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) @@ -572,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) @@ -587,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)])) @@ -606,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)])) @@ -650,16 +650,16 @@ 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) let stmt = exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0, 0])) @@ -668,8 +668,8 @@ describe('execute txs', () => { 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) let stmt = exec.instantiate(pkgStmt.idx, 0, new Uint8Array([0, 1, 5, 104, 101, 108, 108, 111])) @@ -678,8 +678,8 @@ describe('execute txs', () => { 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])) @@ -716,17 +716,17 @@ 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); @@ -740,8 +740,8 @@ describe('execute txs', () => { 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); @@ -756,8 +756,8 @@ describe('execute txs', () => { 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); @@ -775,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} @@ -803,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])) @@ -817,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])) @@ -832,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) @@ -888,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])) @@ -903,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])) @@ -917,10 +917,10 @@ 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])) @@ -934,19 +934,19 @@ describe('execute txs', () => { }) }); - 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) @@ -954,8 +954,8 @@ 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])) @@ -965,9 +965,9 @@ describe('execute txs', () => { 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()) @@ -979,14 +979,14 @@ 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])) @@ -996,9 +996,9 @@ describe('execute txs', () => { 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) @@ -1007,9 +1007,9 @@ 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', []])) @@ -1017,17 +1017,17 @@ describe('execute txs', () => { 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([userPriv]) + 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', () => { - const {exec: exec1} = fundedExec([]) + 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])) diff --git a/packages/vm/test/util.ts b/packages/vm/test/util.ts index fa7dd4c1..36e2ab82 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"; @@ -13,7 +13,7 @@ import {ExecOpts} from "../src/export-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) + 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 => { From e225243f5bfdf05cfff5801f19f7f622f89d9b35 Mon Sep 17 00:00:00 2001 From: migue Date: Tue, 19 Dec 2023 16:39:01 -0300 Subject: [PATCH 12/17] docs for statement-result.ts --- packages/vm/src/statement-result.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) 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'); From f5844ff216f8a8953e19a7636c75c4e9baec2460 Mon Sep 17 00:00:00 2001 From: migue Date: Tue, 19 Dec 2023 18:56:07 -0300 Subject: [PATCH 13/17] more renames and docs added --- ...anslator.ts => arguments-pre-processor.ts} | 40 +++++---- packages/vm/src/calculate-package-hash.ts | 13 +++ packages/vm/src/calculate-package-id.ts | 6 -- packages/vm/src/errors.ts | 13 +++ .../vm/src/exec-context/ex-tx-exec-context.ts | 4 +- .../vm/src/{export-opts.ts => exec-opts.ts} | 3 + packages/vm/src/execution-result.ts | 4 +- packages/vm/src/index.ts | 2 +- packages/vm/src/jig-init-params.ts | 3 + packages/vm/src/jig-ref.ts | 88 +++++++++++++++---- packages/vm/src/memory-proxy.ts | 52 ++++++++++- packages/vm/src/memory.ts | 0 packages/vm/src/metering/measurements.ts | 2 +- packages/vm/src/tx-execution.ts | 8 +- packages/vm/src/vm.ts | 8 +- packages/vm/test/package-id-test.spec.ts | 10 +-- packages/vm/test/specific-scenarios.spec.ts | 2 +- packages/vm/test/tx-execution.spec.ts | 2 +- packages/vm/test/util.ts | 2 +- 19 files changed, 197 insertions(+), 65 deletions(-) rename packages/vm/src/{args-translator.ts => arguments-pre-processor.ts} (68%) create mode 100644 packages/vm/src/calculate-package-hash.ts delete mode 100644 packages/vm/src/calculate-package-id.ts rename packages/vm/src/{export-opts.ts => exec-opts.ts} (98%) delete mode 100644 packages/vm/src/memory.ts diff --git a/packages/vm/src/args-translator.ts b/packages/vm/src/arguments-pre-processor.ts similarity index 68% rename from packages/vm/src/args-translator.ts rename to packages/vm/src/arguments-pre-processor.ts index 14738385..a7facdf6 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 the references 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,25 @@ 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) { + 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 +140,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 41e83504..26d244ba 100644 --- a/packages/vm/src/errors.ts +++ b/packages/vm/src/errors.ts @@ -1,4 +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 {} + +/** + * 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/exec-context/ex-tx-exec-context.ts b/packages/vm/src/exec-context/ex-tx-exec-context.ts index 7e6d5043..ab7508e0 100644 --- a/packages/vm/src/exec-context/ex-tx-exec-context.ts +++ b/packages/vm/src/exec-context/ex-tx-exec-context.ts @@ -35,7 +35,7 @@ import {WasmContainer} from "../wasm-container.js"; import {ExtendedTx} from "./extended-tx.js"; import {CompileFn} from "../vm.js"; import {ExecutionError} from "../errors.js"; -import {calculatePackageId} from "../calculate-package-id.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"; @@ -52,7 +52,7 @@ export class ExTxExecContext implements ExecContext { } async compile (entries: string[], sources: Map): Promise { - const id = calculatePackageId(entries, sources) + const id = calculatePackageHash(entries, sources) const result = await this.compileFn(entries, sources, new Map()) 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 4d403b49..3853a380 100644 --- a/packages/vm/src/execution-result.ts +++ b/packages/vm/src/execution-result.ts @@ -2,7 +2,7 @@ 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/pkg-data.js"; @@ -23,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 6915c62f..0bc1c9ba 100644 --- a/packages/vm/src/index.ts +++ b/packages/vm/src/index.ts @@ -1,6 +1,6 @@ export * from './vm.js' export * from './storage/mem-storage.js' -export * from './calculate-package-id.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"; diff --git a/packages/vm/src/jig-init-params.ts b/packages/vm/src/jig-init-params.ts index b4e514b0..7f595786 100644 --- a/packages/vm/src/jig-init-params.ts +++ b/packages/vm/src/jig-init-params.ts @@ -5,6 +5,9 @@ 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/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/metering/measurements.ts b/packages/vm/src/metering/measurements.ts index 2240d044..82bfadaa 100644 --- a/packages/vm/src/metering/measurements.ts +++ b/packages/vm/src/metering/measurements.ts @@ -1,5 +1,5 @@ import {DiscretCounter} from "./discret-counter.js"; -import {ExecOpts} from "../export-opts.js"; +import {ExecOpts} from "../exec-opts.js"; export class Measurements { movedData: DiscretCounter; diff --git a/packages/vm/src/tx-execution.ts b/packages/vm/src/tx-execution.ts index 6aa4157b..589f7e0c 100644 --- a/packages/vm/src/tx-execution.ts +++ b/packages/vm/src/tx-execution.ts @@ -17,10 +17,10 @@ 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"; @@ -446,8 +446,8 @@ class TxExecution { * @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) diff --git a/packages/vm/src/vm.ts b/packages/vm/src/vm.ts index d3136b16..e7092010 100644 --- a/packages/vm/src/vm.ts +++ b/packages/vm/src/vm.ts @@ -1,7 +1,7 @@ import {MemStorage} from "./storage/mem-storage.js"; import {CompilerResult, PackageParser, writeDependency} from '@aldea/compiler' import {abiFromBin, Address, base16, BCS, OpCode, Output, Pointer, Tx, util} from "@aldea/core"; -import {calculatePackageId} from "./calculate-package-id.js"; +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' @@ -24,7 +24,7 @@ import { 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"; @@ -136,7 +136,7 @@ export class VM { * @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), @@ -177,7 +177,7 @@ export class VM { const entries = ['index.ts']; const hash = defaultId ? defaultId - : calculatePackageId(entries, sources) + : calculatePackageHash(entries, sources) const id = base16.encode(hash) if (this.storage.getPkg(id).isPresent()) { return hash 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 ce256d32..40bbf950 100644 --- a/packages/vm/test/specific-scenarios.spec.ts +++ b/packages/vm/test/specific-scenarios.spec.ts @@ -5,7 +5,7 @@ import {ArgsBuilder, buildVm, fundedExecFactoryFactory, parseOutput} from "./uti import {TxExecution} from "../src/tx-execution.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', () => { diff --git a/packages/vm/test/tx-execution.spec.ts b/packages/vm/test/tx-execution.spec.ts index 87752422..c13669ae 100644 --- a/packages/vm/test/tx-execution.spec.ts +++ b/packages/vm/test/tx-execution.spec.ts @@ -10,7 +10,7 @@ import {StatementResult} from "../src/statement-result.js"; import {ExecutionResult} from "../src/index.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"; diff --git a/packages/vm/test/util.ts b/packages/vm/test/util.ts index 36e2ab82..9c83728e 100644 --- a/packages/vm/test/util.ts +++ b/packages/vm/test/util.ts @@ -9,7 +9,7 @@ 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)); From ebc0b26a21404f30da9e7ec82b5d187163210eb9 Mon Sep 17 00:00:00 2001 From: migue Date: Wed, 20 Dec 2023 09:34:03 -0300 Subject: [PATCH 14/17] better wording --- packages/vm/src/arguments-pre-processor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vm/src/arguments-pre-processor.ts b/packages/vm/src/arguments-pre-processor.ts index a7facdf6..585f73a9 100644 --- a/packages/vm/src/arguments-pre-processor.ts +++ b/packages/vm/src/arguments-pre-processor.ts @@ -19,7 +19,7 @@ export class ArgumentsPreProcessor { } /** - * Solves the references in the encoded data by replacing the references with their corresponding values. + * 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. From 5bf6d256b3561fa71080fc14ffc159868f730901 Mon Sep 17 00:00:00 2001 From: migue Date: Wed, 20 Dec 2023 09:35:50 -0300 Subject: [PATCH 15/17] more docs --- packages/vm/src/arguments-pre-processor.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/vm/src/arguments-pre-processor.ts b/packages/vm/src/arguments-pre-processor.ts index 585f73a9..e9d4b32d 100644 --- a/packages/vm/src/arguments-pre-processor.ts +++ b/packages/vm/src/arguments-pre-processor.ts @@ -126,6 +126,14 @@ export class ArgumentsPreProcessor { } } + /** + * 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) From b61f41c247403a402d439743fa521073a239a901 Mon Sep 17 00:00:00 2001 From: migue Date: Wed, 20 Dec 2023 17:19:17 -0300 Subject: [PATCH 16/17] more docs --- packages/vm/src/locks/address-lock.ts | 3 ++ packages/vm/src/locks/from-core-lock.ts | 5 +++ packages/vm/src/locks/frozen-lock.ts | 4 +++ packages/vm/src/locks/jig-lock.ts | 3 ++ packages/vm/src/locks/lock.ts | 10 ++++++ packages/vm/src/locks/open-lock.ts | 3 ++ .../vm/src/memory/abi-helpers/abi-access.ts | 5 +++ packages/vm/src/memory/bigint-buf.ts | 4 +++ .../{discret-counter.ts => hydro-counter.ts} | 14 +++++++- packages/vm/src/metering/measurements.ts | 34 +++++++++++-------- packages/vm/src/storage/mem-storage.ts | 3 ++ packages/vm/src/storage/pkg-data.ts | 3 ++ 12 files changed, 75 insertions(+), 16 deletions(-) rename packages/vm/src/metering/{discret-counter.ts => hydro-counter.ts} (63%) 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 a092f49b..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; diff --git a/packages/vm/src/locks/open-lock.ts b/packages/vm/src/locks/open-lock.ts index 874d2698..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)); diff --git a/packages/vm/src/memory/abi-helpers/abi-access.ts b/packages/vm/src/memory/abi-helpers/abi-access.ts index 3dd48c07..9ef5311d 100644 --- a/packages/vm/src/memory/abi-helpers/abi-access.ts +++ b/packages/vm/src/memory/abi-helpers/abi-access.ts @@ -13,6 +13,11 @@ import { 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/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/metering/discret-counter.ts b/packages/vm/src/metering/hydro-counter.ts similarity index 63% rename from packages/vm/src/metering/discret-counter.ts rename to packages/vm/src/metering/hydro-counter.ts index 0b67c09d..277125e8 100644 --- a/packages/vm/src/metering/discret-counter.ts +++ b/packages/vm/src/metering/hydro-counter.ts @@ -1,6 +1,18 @@ import {ExecutionError} from "../errors.js"; -export class DiscretCounter { +/** + * 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 diff --git a/packages/vm/src/metering/measurements.ts b/packages/vm/src/metering/measurements.ts index 82bfadaa..a6f60624 100644 --- a/packages/vm/src/metering/measurements.ts +++ b/packages/vm/src/metering/measurements.ts @@ -1,23 +1,27 @@ -import {DiscretCounter} from "./discret-counter.js"; +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: DiscretCounter; - wasmExecuted: DiscretCounter; - numContainers: DiscretCounter; - numSigs: DiscretCounter; - originChecks: DiscretCounter; - newJigs: DiscretCounter; - deploys: DiscretCounter; + movedData: HydroCounter; + wasmExecuted: HydroCounter; + numContainers: HydroCounter; + numSigs: HydroCounter; + originChecks: HydroCounter; + newJigs: HydroCounter; + deploys: HydroCounter; 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) + 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 () { diff --git a/packages/vm/src/storage/mem-storage.ts b/packages/vm/src/storage/mem-storage.ts index c20f280a..02dcd440 100644 --- a/packages/vm/src/storage/mem-storage.ts +++ b/packages/vm/src/storage/mem-storage.ts @@ -5,6 +5,9 @@ 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 diff --git a/packages/vm/src/storage/pkg-data.ts b/packages/vm/src/storage/pkg-data.ts index 4d8b9da1..d59e6bc9 100644 --- a/packages/vm/src/storage/pkg-data.ts +++ b/packages/vm/src/storage/pkg-data.ts @@ -1,6 +1,9 @@ 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 From 334a1216de821f29155b3baad96b97eb14001408 Mon Sep 17 00:00:00 2001 From: migue Date: Wed, 20 Dec 2023 17:22:44 -0300 Subject: [PATCH 17/17] little changes in readme --- packages/vm/README.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) 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.