diff --git a/src/contract/deploy/CreateContract.ts b/src/contract/deploy/CreateContract.ts index 839e7496d..63404c39d 100644 --- a/src/contract/deploy/CreateContract.ts +++ b/src/contract/deploy/CreateContract.ts @@ -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'; @@ -18,9 +19,10 @@ export const emptyTransfer: ArTransfer = { winstonQty: '0' }; -export interface CommonContractData { +export interface CommonContractData { wallet: ArWallet | SignatureType; - initState: string | Buffer; + stateFormat: T; + initState: T extends SerializationFormat.JSON ? string : Buffer; tags?: Tags; transfer?: ArTransfer; data?: { @@ -29,13 +31,13 @@ export interface CommonContractData { }; } -export interface ContractData extends CommonContractData { +export interface ContractData extends CommonContractData { src: string | Buffer; wasmSrcCodeDir?: string; wasmGlueCode?: string; } -export interface FromSrcTxContractData extends CommonContractData { +export interface FromSrcTxContractData extends CommonContractData { srcTxId: string; } @@ -44,10 +46,10 @@ export interface ContractDeploy { srcTxId?: string; } -export interface CreateContract extends Source { - deploy(contractData: ContractData, disableBundling?: boolean): Promise; +export interface CreateContract extends Source { + deploy(contractData: ContractData, disableBundling?: boolean): Promise; - deployFromSourceTx(contractData: FromSrcTxContractData, disableBundling?: boolean): Promise; + deployFromSourceTx(contractData: FromSrcTxContractData, disableBundling?: boolean): Promise; deployBundled(rawDataItem: Buffer): Promise; } diff --git a/src/contract/deploy/impl/DefaultCreateContract.ts b/src/contract/deploy/impl/DefaultCreateContract.ts index f6614bd22..6cf4012a9 100644 --- a/src/contract/deploy/impl/DefaultCreateContract.ts +++ b/src/contract/deploy/impl/DefaultCreateContract.ts @@ -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 implements CreateContract { private readonly logger = LoggerFactory.INST.create('DefaultCreateContract'); private readonly source: SourceImpl; @@ -21,8 +23,11 @@ export class DefaultCreateContract implements CreateContract { this.source = new SourceImpl(this.warp); } - async deploy(contractData: ContractData, disableBundling?: boolean): Promise { - const { wallet, initState, tags, transfer, data } = contractData; + async deploy( + contractData: ContractData, + disableBundling?: boolean + ): Promise { + const { wallet, stateFormat, initState, tags, transfer, data } = contractData; const effectiveUseBundler = disableBundling == undefined ? this.warp.definitionLoader.type() == 'warp' : !disableBundling; @@ -38,6 +43,7 @@ export class DefaultCreateContract implements CreateContract { { srcTxId: srcTx.id, wallet, + stateFormat, initState, tags, transfer, @@ -48,13 +54,13 @@ export class DefaultCreateContract implements CreateContract { ); } - async deployFromSourceTx( - contractData: FromSrcTxContractData, + async deployFromSourceTx( + contractData: FromSrcTxContractData, disableBundling?: boolean, srcTx: Transaction = null ): Promise { 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; @@ -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 . 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') { diff --git a/src/core/Warp.ts b/src/core/Warp.ts index 19b722bf1..98489241f 100644 --- a/src/core/Warp.ts +++ b/src/core/Warp.ts @@ -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'; @@ -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; readonly testing: Testing; private readonly plugins: Map> = new Map(); @@ -73,11 +73,17 @@ export class Warp { return new HandlerBasedContract(contractTxId, this, callingContract, innerCallData); } - async deploy(contractData: ContractData, disableBundling?: boolean): Promise { + async deploy( + contractData: ContractData, + disableBundling?: boolean + ): Promise { return await this.createContract.deploy(contractData, disableBundling); } - async deployFromSourceTx(contractData: FromSrcTxContractData, disableBundling?: boolean): Promise { + async deployFromSourceTx( + contractData: FromSrcTxContractData, + disableBundling?: boolean + ): Promise { return await this.createContract.deployFromSourceTx(contractData, disableBundling); } diff --git a/src/core/modules/StateEvaluator.ts b/src/core/modules/StateEvaluator.ts index add07fe8f..1081e21fa 100644 --- a/src/core/modules/StateEvaluator.ts +++ b/src/core/modules/StateEvaluator.ts @@ -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'; @@ -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; + +export const Deserializers = { + [SerializationFormat.JSON]: JSON.parse, + [SerializationFormat.MSGPACK]: unpack +} as const satisfies Record; + // 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 @@ -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; } diff --git a/src/core/modules/impl/CacheableStateEvaluator.ts b/src/core/modules/impl/CacheableStateEvaluator.ts index 41ee407d7..26098d723 100644 --- a/src/core/modules/impl/CacheableStateEvaluator.ts +++ b/src/core/modules/impl/CacheableStateEvaluator.ts @@ -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'; @@ -35,11 +35,12 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator { currentTx: CurrentTx[] ): Promise>> { 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; } @@ -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 diff --git a/src/core/modules/impl/DefaultStateEvaluator.ts b/src/core/modules/impl/DefaultStateEvaluator.ts index 303874342..74f22bdba 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -53,8 +53,13 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { executionContext: ExecutionContext>, currentTx: CurrentTx[] ): Promise>> { - 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; @@ -62,7 +67,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { const validity = baseState.validity; const errorMessages = baseState.errorMessages; - executionContext?.handler.initState(currentState); + executionContext?.handler.initState(currentState, serializationFormat); const depth = executionContext.contract.callDepth(); @@ -76,7 +81,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { let lastConfirmedTxState: { tx: GQLNodeInterface; state: EvalStateResult } = null; const missingInteractionsLength = missingInteractions.length; - executionContext.handler.initState(currentState); + executionContext.handler.initState(currentState, serializationFormat); const evmSignatureVerificationPlugin = warp.hasPlugin('evm-signature-verification') ? warp.loadPlugin>('evm-signature-verification') @@ -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]) { diff --git a/src/core/modules/impl/HandlerExecutorFactory.ts b/src/core/modules/impl/HandlerExecutorFactory.ts index 0e23bd29c..94ac75367 100644 --- a/src/core/modules/impl/HandlerExecutorFactory.ts +++ b/src/core/modules/impl/HandlerExecutorFactory.ts @@ -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'; @@ -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 { @@ -105,6 +106,18 @@ export class HandlerExecutorFactory implements ExecutorFactory 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, @@ -245,7 +258,7 @@ export interface HandlerApi { interactionData: InteractionData ): Promise>; - initState(state: State): void; + initState(state: State, format: SerializationFormat): void; } export type HandlerFunction = ( diff --git a/src/core/modules/impl/handler/AbstractContractHandler.ts b/src/core/modules/impl/handler/AbstractContractHandler.ts index 0c4a82ff0..d25ed9ba1 100644 --- a/src/core/modules/impl/handler/AbstractContractHandler.ts +++ b/src/core/modules/impl/handler/AbstractContractHandler.ts @@ -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'; @@ -27,7 +27,7 @@ export abstract class AbstractContractHandler implements HandlerApi ): Promise>; - abstract initState(state: State): void; + abstract initState(state: State, format: SerializationFormat): void; async dispose(): Promise { // noop by default; diff --git a/src/core/modules/impl/handler/WasmHandlerApi.ts b/src/core/modules/impl/handler/WasmHandlerApi.ts index c2b29ba84..8c74b71d4 100644 --- a/src/core/modules/impl/handler/WasmHandlerApi.ts +++ b/src/core/modules/impl/handler/WasmHandlerApi.ts @@ -3,11 +3,17 @@ import { unpack, pack } from 'msgpackr'; import { ContractDefinition } from '../../../../core/ContractDefinition'; import { ExecutionContext } from '../../../../core/ExecutionContext'; -import { EvalStateResult } from '../../../../core/modules/StateEvaluator'; +import { + Deserializers, + EvalStateResult, + SerializationFormat, + Serializers +} from '../../../../core/modules/StateEvaluator'; import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global'; import stringify from 'safe-stable-stringify'; -import { InteractionData, InteractionResult } from '../HandlerExecutorFactory'; +import { ContractInteraction, InteractionData, InteractionResult } from '../HandlerExecutorFactory'; import { AbstractContractHandler } from './AbstractContractHandler'; +import { exhaustive } from 'utils/utils'; export class WasmHandlerApi extends AbstractContractHandler { constructor( @@ -26,21 +32,22 @@ export class WasmHandlerApi extends AbstractContractHandler { ): Promise> { try { const { interaction, interactionTx, currentTx } = interactionData; + const { gasLimit, wasmSerializationFormat } = executionContext.evaluationOptions; this.swGlobal._activeTx = interactionTx; this.swGlobal.caller = interaction.caller; // either contract tx id (for internal writes) or transaction.owner - this.swGlobal.gasLimit = executionContext.evaluationOptions.gasLimit; + this.swGlobal.gasLimit = gasLimit; this.swGlobal.gasUsed = 0; this.assignReadContractState(executionContext, currentTx, currentResult, interactionTx); this.assignWrite(executionContext, currentTx); - const handlerResult = await this.doHandle(interaction); + const handlerResult = await this.doHandle(interaction, wasmSerializationFormat); return { type: 'ok', result: handlerResult, - state: this.doGetCurrentState(), // TODO: return only at the end of evaluation and when caching is required + state: this.doGetCurrentState(wasmSerializationFormat), // TODO: return only at the end of evaluation and when caching is required gasUsed: this.swGlobal.gasUsed }; } catch (e) { @@ -75,7 +82,8 @@ export class WasmHandlerApi extends AbstractContractHandler { } } - initState(state: State): void { + // TODO:noom support SerializationFormat here too + initState(state: State, format: SerializationFormat): void { switch (this.contractDefinition.srcWasmLang) { case 'assemblyscript': { const statePtr = this.wasmExports.__newString(stringify(state)); @@ -83,7 +91,7 @@ export class WasmHandlerApi extends AbstractContractHandler { break; } case 'rust': { - this.wasmExports.initState(pack(state)); + this.wasmExports.initState(Serializers[format](state)); break; } case 'go': { @@ -96,7 +104,7 @@ export class WasmHandlerApi extends AbstractContractHandler { } } - private async doHandle(action: any): Promise { + private async doHandle(action: ContractInteraction, format: SerializationFormat): Promise { switch (this.contractDefinition.srcWasmLang) { case 'assemblyscript': { const actionPtr = this.wasmExports.__newString(stringify(action.input)); @@ -106,7 +114,8 @@ export class WasmHandlerApi extends AbstractContractHandler { return JSON.parse(result); } case 'rust': { - let handleResult = await this.wasmExports.handle(pack(action.input)); + const handleResult = await this.wasmExports.handle(Serializers[format](action.input)); + if (!handleResult) { return; } @@ -140,14 +149,14 @@ export class WasmHandlerApi extends AbstractContractHandler { } } - private doGetCurrentState(): State { + private doGetCurrentState(format: SerializationFormat): State { switch (this.contractDefinition.srcWasmLang) { case 'assemblyscript': { const currentStatePtr = this.wasmExports.currentState(); return JSON.parse(this.wasmExports.__getString(currentStatePtr)); } case 'rust': { - return unpack(this.wasmExports.currentState()); + return Deserializers[format](this.wasmExports.currentState()); } case 'go': { const result = this.wasmExports.currentState(); diff --git a/src/core/modules/impl/wasm/rust-wasm-imports.ts b/src/core/modules/impl/wasm/rust-wasm-imports-json.ts similarity index 99% rename from src/core/modules/impl/wasm/rust-wasm-imports.ts rename to src/core/modules/impl/wasm/rust-wasm-imports-json.ts index 86609b048..0adaae70e 100644 --- a/src/core/modules/impl/wasm/rust-wasm-imports.ts +++ b/src/core/modules/impl/wasm/rust-wasm-imports-json.ts @@ -6,7 +6,7 @@ import { LoggerFactory } from '../../../../logging/LoggerFactory'; // note: this is (somewhat heavily) modified code // of the js that is normally generated by the wasm-bindgen -export const rustWasmImports = (swGlobal, wbindgenImports, wasmInstance, dtorValue): any => { +export const rustWasmImportsJson = (swGlobal, wbindgenImports, wasmInstance, dtorValue): any => { const wasmLogger = LoggerFactory.INST.create('WASM:Rust'); // the raw functions, that we want to make available from the diff --git a/src/core/modules/impl/wasm/rust-wasm-imports-msgpack.ts b/src/core/modules/impl/wasm/rust-wasm-imports-msgpack.ts index 075415127..00ffb77cc 100644 --- a/src/core/modules/impl/wasm/rust-wasm-imports-msgpack.ts +++ b/src/core/modules/impl/wasm/rust-wasm-imports-msgpack.ts @@ -6,7 +6,7 @@ import { LoggerFactory } from '../../../../logging/LoggerFactory'; // note: this is (somewhat heavily) modified code // of the js that is normally generated by the wasm-bindgen -export const rustWasmImports = (swGlobal, wbindgenImports, wasmInstance, dtorValue): any => { +export const rustWasmImportsMsgpack = (swGlobal, wbindgenImports, wasmInstance, dtorValue): any => { const wasmLogger = LoggerFactory.INST.create('WASM:Rust'); // the raw functions, that we want to make available from the diff --git a/src/plugins/Evolve.ts b/src/plugins/Evolve.ts index d90fe4437..e95e628ca 100644 --- a/src/plugins/Evolve.ts +++ b/src/plugins/Evolve.ts @@ -52,7 +52,7 @@ export class Evolve implements ExecutionContextModifier { //FIXME: side-effect... executionContext.contractDefinition = newContractDefinition; executionContext.handler = newHandler; - executionContext.handler.initState(state); + executionContext.handler.initState(state, executionContext.evaluationOptions.wasmSerializationFormat); this.logger.debug('evolved to:', { evolve: evolvedSrcTxId, newSrcTxId: executionContext.contractDefinition.srcTxId, diff --git a/src/utils/utils.ts b/src/utils/utils.ts index b4192c8af..8b38357c2 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -3,6 +3,10 @@ import copy from 'fast-copy'; import { Buffer } from 'redstone-isomorphic'; import { randomUUID } from 'crypto'; +export const exhaustive = (_: never) => { + throw new Error('Exhaustive check'); +}; + export const sleep = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); };