Skip to content

Commit

Permalink
feat: allow choosing rust wasm contracts serialization format
Browse files Browse the repository at this point in the history
  • Loading branch information
noomly committed Dec 22, 2022
1 parent 8355d89 commit 240f6b9
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 47 deletions.
16 changes: 9 additions & 7 deletions src/contract/deploy/CreateContract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { JWKInterface } from 'arweave/node/lib/wallet';
import { SerializationFormat } from 'core/modules/StateEvaluator';
import { SignatureType } from '../../contract/Signature';
import { Source } from './Source';

Expand All @@ -18,9 +19,10 @@ export const emptyTransfer: ArTransfer = {
winstonQty: '0'
};

export interface CommonContractData {
export interface CommonContractData<T extends SerializationFormat> {
wallet: ArWallet | SignatureType;
initState: string | Buffer;
stateFormat: T;
initState: T extends SerializationFormat.JSON ? string : Buffer;
tags?: Tags;
transfer?: ArTransfer;
data?: {
Expand All @@ -29,13 +31,13 @@ export interface CommonContractData {
};
}

export interface ContractData extends CommonContractData {
export interface ContractData<T extends SerializationFormat> extends CommonContractData<T> {
src: string | Buffer;
wasmSrcCodeDir?: string;
wasmGlueCode?: string;
}

export interface FromSrcTxContractData extends CommonContractData {
export interface FromSrcTxContractData<T extends SerializationFormat> extends CommonContractData<T> {
srcTxId: string;
}

Expand All @@ -44,10 +46,10 @@ export interface ContractDeploy {
srcTxId?: string;
}

export interface CreateContract extends Source {
deploy(contractData: ContractData, disableBundling?: boolean): Promise<ContractDeploy>;
export interface CreateContract<T extends SerializationFormat> extends Source {
deploy(contractData: ContractData<T>, disableBundling?: boolean): Promise<ContractDeploy>;

deployFromSourceTx(contractData: FromSrcTxContractData, disableBundling?: boolean): Promise<ContractDeploy>;
deployFromSourceTx(contractData: FromSrcTxContractData<T>, disableBundling?: boolean): Promise<ContractDeploy>;

deployBundled(rawDataItem: Buffer): Promise<ContractDeploy>;
}
41 changes: 34 additions & 7 deletions src/contract/deploy/impl/DefaultCreateContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { LoggerFactory } from '../../../logging/LoggerFactory';
import { CreateContract, ContractData, ContractDeploy, FromSrcTxContractData, ArWallet } from '../CreateContract';
import { SourceData, SourceImpl } from './SourceImpl';
import { Buffer } from 'redstone-isomorphic';
import { SerializationFormat } from 'core/modules/StateEvaluator';
import { exhaustive } from 'utils/utils';

export class DefaultCreateContract implements CreateContract {
export class DefaultCreateContract<T extends SerializationFormat> implements CreateContract<T> {
private readonly logger = LoggerFactory.INST.create('DefaultCreateContract');
private readonly source: SourceImpl;

Expand All @@ -21,8 +23,11 @@ export class DefaultCreateContract implements CreateContract {
this.source = new SourceImpl(this.warp);
}

async deploy(contractData: ContractData, disableBundling?: boolean): Promise<ContractDeploy> {
const { wallet, initState, tags, transfer, data } = contractData;
async deploy<T extends SerializationFormat>(
contractData: ContractData<T>,
disableBundling?: boolean
): Promise<ContractDeploy> {
const { wallet, stateFormat, initState, tags, transfer, data } = contractData;

const effectiveUseBundler =
disableBundling == undefined ? this.warp.definitionLoader.type() == 'warp' : !disableBundling;
Expand All @@ -38,6 +43,7 @@ export class DefaultCreateContract implements CreateContract {
{
srcTxId: srcTx.id,
wallet,
stateFormat,
initState,
tags,
transfer,
Expand All @@ -48,13 +54,13 @@ export class DefaultCreateContract implements CreateContract {
);
}

async deployFromSourceTx(
contractData: FromSrcTxContractData,
async deployFromSourceTx<T extends SerializationFormat>(
contractData: FromSrcTxContractData<T>,
disableBundling?: boolean,
srcTx: Transaction = null
): Promise<ContractDeploy> {
this.logger.debug('Creating new contract from src tx');
const { wallet, srcTxId, initState, tags, transfer, data } = contractData;
const { wallet, srcTxId, stateFormat, initState, tags, transfer, data } = contractData;
this.signature = new Signature(this.warp, wallet);
const signer = this.signature.signer;

Expand Down Expand Up @@ -90,7 +96,28 @@ export class DefaultCreateContract implements CreateContract {
typeof initState === 'string' ? initState : new TextDecoder().decode(initState)
);
} else {
contractTX.addTag(SmartWeaveTags.CONTENT_TYPE, 'application/json');
let contentType: undefined | string;

switch (stateFormat) {
case SerializationFormat.JSON:
contentType = 'application/json';
break;
case SerializationFormat.MSGPACK:
// NOTE: There is still no officially registered Media Type for Messagepack and there are
// apparently multiple different versions used in the wild like:
// - application/msgpack
// - application/x-msgpack
// - application/x.msgpack
// - [...]
// See <https://github.com/msgpack/msgpack/issues/194>. I've decided to use the first one
// as it looks like the one that makes the most sense.
contentType = 'application/msgpack';
break;
default:
return exhaustive(stateFormat);
}

contractTX.addTag(SmartWeaveTags.CONTENT_TYPE, contentType);
}

if (this.warp.environment === 'testnet') {
Expand Down
14 changes: 10 additions & 4 deletions src/core/Warp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { DefinitionLoader } from './modules/DefinitionLoader';
import { ExecutorFactory } from './modules/ExecutorFactory';
import { HandlerApi } from './modules/impl/HandlerExecutorFactory';
import { InteractionsLoader } from './modules/InteractionsLoader';
import { EvalStateResult, StateEvaluator } from './modules/StateEvaluator';
import { EvalStateResult, SerializationFormat, StateEvaluator } from './modules/StateEvaluator';
import { WarpBuilder } from './WarpBuilder';
import { WarpPluginType, WarpPlugin, knownWarpPlugins } from './WarpPlugin';
import { SortKeyCache } from '../cache/SortKeyCache';
Expand All @@ -39,7 +39,7 @@ export class Warp {
/**
* @deprecated createContract will be a private field, please use its methods directly e.g. await warp.deploy(...)
*/
readonly createContract: CreateContract;
readonly createContract: CreateContract<SerializationFormat>;
readonly testing: Testing;

private readonly plugins: Map<WarpPluginType, WarpPlugin<unknown, unknown>> = new Map();
Expand Down Expand Up @@ -73,11 +73,17 @@ export class Warp {
return new HandlerBasedContract<State>(contractTxId, this, callingContract, innerCallData);
}

async deploy(contractData: ContractData, disableBundling?: boolean): Promise<ContractDeploy> {
async deploy<T extends SerializationFormat>(
contractData: ContractData<T>,
disableBundling?: boolean
): Promise<ContractDeploy> {
return await this.createContract.deploy(contractData, disableBundling);
}

async deployFromSourceTx(contractData: FromSrcTxContractData, disableBundling?: boolean): Promise<ContractDeploy> {
async deployFromSourceTx<T extends SerializationFormat>(
contractData: FromSrcTxContractData<T>,
disableBundling?: boolean
): Promise<ContractDeploy> {
return await this.createContract.deployFromSourceTx(contractData, disableBundling);
}

Expand Down
27 changes: 27 additions & 0 deletions src/core/modules/StateEvaluator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { pack, unpack } from 'msgpackr';
import stringify from 'safe-stable-stringify';

import { SortKeyCache, SortKeyCacheResult } from '../../cache/SortKeyCache';
import { CurrentTx } from '../../contract/Contract';
import { ExecutionContext } from '../../core/ExecutionContext';
Expand Down Expand Up @@ -138,8 +141,25 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
throwOnInternalWriteError = true;

cacheEveryNInteractions = -1;

wasmSerializationFormat = SerializationFormat.JSON;
}

export enum SerializationFormat {
JSON = 'json',
MSGPACK = 'msgpack'
}

export const Serializers = {
[SerializationFormat.JSON]: stringify,
[SerializationFormat.MSGPACK]: pack
} as const satisfies Record<SerializationFormat, unknown>;

export const Deserializers = {
[SerializationFormat.JSON]: JSON.parse,
[SerializationFormat.MSGPACK]: unpack
} as const satisfies Record<SerializationFormat, unknown>;

// an interface for the contract EvaluationOptions - can be used to change the behaviour of some features.
export interface EvaluationOptions {
// whether exceptions from given transaction interaction should be ignored
Expand Down Expand Up @@ -214,4 +234,11 @@ export interface EvaluationOptions {
// force SDK to cache the state after evaluating each N interactions
// defaults to -1, which effectively turns off this feature
cacheEveryNInteractions: number;

/**
* What serialization format should be used for the WASM<->JS bridge. Note that changing this
* currently only affects Rust smartcontracts. AssemblyScript and Go smartcontracts will always
* use JSON. Defaults to JSON.
*/
wasmSerializationFormat: SerializationFormat;
}
9 changes: 5 additions & 4 deletions src/core/modules/impl/CacheableStateEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ExecutionContextModifier } from '../../../core/ExecutionContextModifier
import { GQLNodeInterface } from '../../../legacy/gqlResult';
import { LoggerFactory } from '../../../logging/LoggerFactory';
import { indent } from '../../../utils/utils';
import { EvalStateResult } from '../StateEvaluator';
import { EvalStateResult, SerializationFormat } from '../StateEvaluator';
import { DefaultStateEvaluator } from './DefaultStateEvaluator';
import { HandlerApi } from './HandlerExecutorFactory';
import { genesisSortKey } from './LexicographicalInteractionsSorter';
Expand Down Expand Up @@ -35,11 +35,12 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
currentTx: CurrentTx[]
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
const cachedState = executionContext.cachedState;
const { wasmSerializationFormat: serializationFormat } = executionContext.evaluationOptions;
if (cachedState && cachedState.sortKey == executionContext.requestedSortKey) {
this.cLogger.info(
`Exact cache hit for sortKey ${executionContext?.contractDefinition?.txId}:${cachedState.sortKey}`
);
executionContext.handler?.initState(cachedState.cachedValue.state);
executionContext.handler?.initState(cachedState.cachedValue.state, serializationFormat);
return cachedState;
}

Expand Down Expand Up @@ -71,10 +72,10 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
if (missingInteractions.length == 0) {
this.cLogger.info(`No missing interactions ${contractTxId}`);
if (cachedState) {
executionContext.handler?.initState(cachedState.cachedValue.state);
executionContext.handler?.initState(cachedState.cachedValue.state, serializationFormat);
return cachedState;
} else {
executionContext.handler?.initState(executionContext.contractDefinition.initState);
executionContext.handler?.initState(executionContext.contractDefinition.initState, serializationFormat);
this.cLogger.debug('Inserting initial state into cache');
const stateToCache = new EvalStateResult(executionContext.contractDefinition.initState, {}, {});
// no real sort-key - as we're returning the initial state
Expand Down
15 changes: 10 additions & 5 deletions src/core/modules/impl/DefaultStateEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,21 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
executionContext: ExecutionContext<State, HandlerApi<State>>,
currentTx: CurrentTx[]
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
const { ignoreExceptions, stackTrace, internalWrites, cacheEveryNInteractions } =
executionContext.evaluationOptions;
const {
ignoreExceptions,
stackTrace,
internalWrites,
cacheEveryNInteractions,
wasmSerializationFormat: serializationFormat
} = executionContext.evaluationOptions;
const { contract, contractDefinition, sortedInteractions, warp } = executionContext;

let currentState = baseState.state;
let currentSortKey = null;
const validity = baseState.validity;
const errorMessages = baseState.errorMessages;

executionContext?.handler.initState(currentState);
executionContext?.handler.initState(currentState, serializationFormat);

const depth = executionContext.contract.callDepth();

Expand All @@ -76,7 +81,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
let lastConfirmedTxState: { tx: GQLNodeInterface; state: EvalStateResult<State> } = null;

const missingInteractionsLength = missingInteractions.length;
executionContext.handler.initState(currentState);
executionContext.handler.initState(currentState, serializationFormat);

const evmSignatureVerificationPlugin = warp.hasPlugin('evm-signature-verification')
? warp.loadPlugin<GQLNodeInterface, Promise<boolean>>('evm-signature-verification')
Expand Down Expand Up @@ -166,7 +171,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
if (newState !== null) {
currentState = newState.cachedValue.state;
// we need to update the state in the wasm module
executionContext?.handler.initState(currentState);
executionContext?.handler.initState(currentState, serializationFormat);

validity[missingInteraction.id] = newState.cachedValue.validity[missingInteraction.id];
if (newState.cachedValue.errorMessages?.[missingInteraction.id]) {
Expand Down
21 changes: 17 additions & 4 deletions src/core/modules/impl/HandlerExecutorFactory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Arweave from 'arweave';
import loader from '@assemblyscript/loader';
import { asWasmImports } from './wasm/as-wasm-imports';
import { rustWasmImports } from './wasm/rust-wasm-imports-msgpack';
import { rustWasmImportsJson } from './wasm/rust-wasm-imports-json';
import { rustWasmImportsMsgpack } from './wasm/rust-wasm-imports-msgpack';
import { Go } from './wasm/go-wasm-imports';
import * as vm2 from 'vm2';
import { WarpCache } from '../../../cache/WarpCache';
Expand All @@ -12,14 +13,14 @@ import { SmartWeaveGlobal } from '../../../legacy/smartweave-global';
import { Benchmark } from '../../../logging/Benchmark';
import { LoggerFactory } from '../../../logging/LoggerFactory';
import { ExecutorFactory } from '../ExecutorFactory';
import { EvalStateResult, EvaluationOptions } from '../StateEvaluator';
import { EvalStateResult, EvaluationOptions, SerializationFormat } from '../StateEvaluator';
import { JsHandlerApi } from './handler/JsHandlerApi';
import { WasmHandlerApi } from './handler/WasmHandlerApi';
import { normalizeContractSource } from './normalize-source';
import { MemCache } from '../../../cache/impl/MemCache';
import BigNumber from '../../../legacy/bignumber';
import { Warp } from '../../Warp';
import { isBrowser } from '../../../utils/utils';
import { exhaustive, isBrowser } from '../../../utils/utils';
import { Buffer } from 'redstone-isomorphic';

class ContractError extends Error {
Expand Down Expand Up @@ -105,6 +106,18 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
})
.map((imp) => imp.name);

let rustWasmImports;
switch (evaluationOptions.wasmSerializationFormat) {
case SerializationFormat.JSON:
rustWasmImports = rustWasmImportsJson;
break;
case SerializationFormat.MSGPACK:
rustWasmImports = rustWasmImportsMsgpack;
break;
default:
return exhaustive(evaluationOptions.wasmSerializationFormat);
}

const { imports, exports } = rustWasmImports(
swGlobal,
wbindgenImports,
Expand Down Expand Up @@ -245,7 +258,7 @@ export interface HandlerApi<State> {
interactionData: InteractionData<Input>
): Promise<InteractionResult<State, Result>>;

initState(state: State): void;
initState(state: State, format: SerializationFormat): void;
}

export type HandlerFunction<State, Input, Result> = (
Expand Down
4 changes: 2 additions & 2 deletions src/core/modules/impl/handler/AbstractContractHandler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ContractError, CurrentTx } from '../../../../contract/Contract';
import { ContractDefinition } from '../../../../core/ContractDefinition';
import { ExecutionContext } from '../../../../core/ExecutionContext';
import { EvalStateResult } from '../../../../core/modules/StateEvaluator';
import { EvalStateResult, SerializationFormat } from '../../../../core/modules/StateEvaluator';
import { GQLNodeInterface } from '../../../../legacy/gqlResult';
import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global';
import { LoggerFactory } from '../../../../logging/LoggerFactory';
Expand All @@ -27,7 +27,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
interactionData: InteractionData<Input>
): Promise<InteractionResult<State, Result>>;

abstract initState(state: State): void;
abstract initState(state: State, format: SerializationFormat): void;

async dispose(): Promise<void> {
// noop by default;
Expand Down
Loading

0 comments on commit 240f6b9

Please sign in to comment.