Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/mocknet/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export async function buildApp (argv: ParsedArgs = { _: [] }): Promise<iApp> {

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))
Expand All @@ -138,7 +138,7 @@ export async function buildApp (argv: ParsedArgs = { _: [] }): Promise<iApp> {
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])))
Expand Down
23 changes: 20 additions & 3 deletions packages/vm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -14,7 +18,14 @@ export class ArgsTranslator {
this.abi = abi
}

fix(encoded: Uint8Array, args: AbiArg[]): Uint8Array {
/**
* Solves the references in the encoded data by replacing them with their corresponding values.
*
* @param {Uint8Array} encoded - The encoded data with references.
* @param {AbiArg[]} args - The array of arguments with their types.
* @returns {Uint8Array} - The updated encoded data with solved references.
*/
solveReferences (encoded: Uint8Array, args: AbiArg[]): Uint8Array {
const reader = new BufReader(encoded)
const indexes = reader.readSeq(r => r.readU8())
const into = new BufWriter()
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -102,25 +113,33 @@ export class ArgsTranslator {
const length = from.readULEB()
into.writeULEB(length)
for (let i = 0; i < length; i++) {
this.translateChunk(from, ty.args[0], into)
this.derefChunk(from, ty.args[0], into)
}
}

private translateMap (from: BufReader, ty: AbiType, into: BufWriter) {
private derefMap (from: BufReader, ty: AbiType, into: BufWriter) {
const length = from.readULEB()
into.writeULEB(length)
for (let i = 0; i < length; i++) {
this.translateChunk(from, ty.args[0], into)
this.translateChunk(from, ty.args[1], into)
this.derefChunk(from, ty.args[0], into)
this.derefChunk(from, ty.args[1], into)
}
}

private translateComplexType(from: BufReader, ty: AbiType, into: BufWriter) {
/**
* At this point the argument might be a plain object or a Jig. Jigs are always sent
* as references, so we need to dereference them. Objects are traversed by its properties.
* @param from
* @param ty
* @param into
* @private
*/
private derefComplexType (from: BufReader, ty: AbiType, into: BufWriter) {
const objDef = this.abi.objectDef(ty.name)

if (objDef.isPresent()) {
for (const fieldTy of objDef.get().fields) {
this.translateChunk(from, fieldTy.type, into)
this.derefChunk(from, fieldTy.type, into)
}
return
} else {
Expand All @@ -129,6 +148,5 @@ export class ArgsTranslator {
const lifted = stmt.lift()
into.writeFixedBytes(lifted)
}

}
}
13 changes: 13 additions & 0 deletions packages/vm/src/calculate-package-hash.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>} sources - The map of source files.
* @return {Uint8Array} The calculated package hash as a Uint8Array.
*/
export function calculatePackageHash (entryPoints: string[], sources: Map<string, string>): Uint8Array {
const data = BCS.pkg.encode([entryPoints.sort(), sources])
return blake3.hash(data)
}
6 changes: 0 additions & 6 deletions packages/vm/src/calculate-package-id.ts

This file was deleted.

14 changes: 13 additions & 1 deletion packages/vm/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
/**
* Runtime error related to lack of permissions.
*/
export class PermissionError extends Error {}

/**
* Runtime error. It might a throw inside the code, a division by zero or
* any kind of breaking in the Aldea protocol.
*/
export class ExecutionError extends Error {}

export class IvariantBroken extends Error {}
/**
* Error raised when a rule that should be followed is broken. For example,
* a jig referencing a jig that does not exist.
*/
export class InvariantBroken extends Error {}

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,74 @@
// }
// }
//
// export class ExTxExecContext implements ExecContext {
import {ExecContext} from "./exec-context.js";
import {abiFromBin, base16, Output, Pointer, PubKey} from "@aldea/core";
import {WasmContainer} from "../wasm-container.js";
import {ExtendedTx} from "./extended-tx.js";
import {CompileFn} from "../vm.js";
import {ExecutionError} from "../errors.js";
import {calculatePackageHash} from "../calculate-package-hash.js";
import {Buffer} from "buffer";
import {Option} from "../support/option.js";
import {PkgData} from "../storage/pkg-data.js";

export class ExTxExecContext implements ExecContext {
exTx: ExtendedTx
private compileFn: CompileFn;
private containers: WasmContainer[];

constructor (exTx: ExtendedTx, containers: WasmContainer[], compileFn: CompileFn) {
this.exTx = exTx
this.containers = containers
this.compileFn = compileFn
}

async compile (entries: string[], sources: Map<string, string>): Promise<PkgData> {
const id = calculatePackageHash(entries, sources)

const result = await this.compileFn(entries, sources, new Map())

return new PkgData(
abiFromBin(result.output.abi),
Buffer.from(result.output.docs || ''),
entries,
id,
new WebAssembly.Module(result.output.wasm),
sources,
result.output.wasm
)
}

inputByOrigin (origin: Pointer): Output {
const output = this.exTx.inputs.find(i => i.origin.equals(origin));
if (!output) {
throw new ExecutionError(`input not provided. Origin:${origin.toString()}`)
}
return output
}

outputById (hash: Uint8Array): Output {
let id = base16.encode(hash)
const output = this.exTx.inputs.find(i => i.id === id);
if (!output) {
throw new ExecutionError(`input not provided. id: ${id}`)
}
return output
}

signers (): PubKey[] {
return this.exTx.tx.signers();
}

txHash (): Uint8Array {
return this.exTx.tx.hash;
}

wasmFromPkgId (pkgId: string): WasmContainer {
return Option.fromNullable(this.containers.find(c => c.id === pkgId)).expect(
new ExecutionError(`Missing package: ${pkgId}`)
)
}
// private exTx: ExtendedTx
// private clock: Clock;
// private pkgs: PkgRepository
Expand Down Expand Up @@ -83,5 +150,4 @@
// wasmFromPkgId(pkgId: Uint8Array): WasmContainer {
// return this.pkgs.wasmForPackageId(pkgId);
// }
// }
//
}
63 changes: 63 additions & 0 deletions packages/vm/src/exec-context/exec-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {Output, Pointer, PubKey} from "@aldea/core";
import {WasmContainer} from "../wasm-container.js";

import {PkgData} from "../storage/pkg-data.js";

/**
* This interface represents everything that is needed for a VM
* to execute a specific transaction.
*
* Notice that all methods are sync. Transaction execution is always sync except for deploys, but
* the VM ensures to simulate sync there too.
*/
export interface ExecContext {
/**
* Retrieves the transaction hash (id) for the transaction being currently executed.
*
* @return {Uint8Array} The transaction hash (32 bytes).
*/
txHash (): Uint8Array

/**
* Retrieves an output from the context, searching by id.
*
* @param {Uint8Array} id - The ID of the output.
* @return {Output} - The Output object representing the state associated with the given output ID.
*/
outputById (id: Uint8Array): Output

/**
* Retrieves an output from context matching given origin.
*
* @param {Pointer} origin - The origin to retrieve the output for.
* @return {Output} - The output corresponding to the provided origin.
*/
inputByOrigin (origin: Pointer): Output

/**
* Retrieves a WasmContainer initialized with the package matching the given id.
*
* @param {string} pkgId - The package ID of the Wasm container to retrieve.
* @return {WasmContainer} - The Wasm container associated with the package ID.
*/
wasmFromPkgId (pkgId: string): WasmContainer

/**
* Compiles the given entries using the provided sources.
*
* @param {string[]} entries - An array of entry strings to compile.
* @param {Map<string, string>} sources - A map containing the source files to compile the entries from.
* @return {Promise<PkgData>} A Promise that resolves to the compiled package data.
*/
compile (entries: string[], sources: Map<string, string>): Promise<PkgData>

/**
* 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[]
}

Loading