Skip to content

Commit 240f6b9

Browse files
committed
feat: allow choosing rust wasm contracts serialization format
1 parent 8355d89 commit 240f6b9

13 files changed

+141
-47
lines changed

src/contract/deploy/CreateContract.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { JWKInterface } from 'arweave/node/lib/wallet';
2+
import { SerializationFormat } from 'core/modules/StateEvaluator';
23
import { SignatureType } from '../../contract/Signature';
34
import { Source } from './Source';
45

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

21-
export interface CommonContractData {
22+
export interface CommonContractData<T extends SerializationFormat> {
2223
wallet: ArWallet | SignatureType;
23-
initState: string | Buffer;
24+
stateFormat: T;
25+
initState: T extends SerializationFormat.JSON ? string : Buffer;
2426
tags?: Tags;
2527
transfer?: ArTransfer;
2628
data?: {
@@ -29,13 +31,13 @@ export interface CommonContractData {
2931
};
3032
}
3133

32-
export interface ContractData extends CommonContractData {
34+
export interface ContractData<T extends SerializationFormat> extends CommonContractData<T> {
3335
src: string | Buffer;
3436
wasmSrcCodeDir?: string;
3537
wasmGlueCode?: string;
3638
}
3739

38-
export interface FromSrcTxContractData extends CommonContractData {
40+
export interface FromSrcTxContractData<T extends SerializationFormat> extends CommonContractData<T> {
3941
srcTxId: string;
4042
}
4143

@@ -44,10 +46,10 @@ export interface ContractDeploy {
4446
srcTxId?: string;
4547
}
4648

47-
export interface CreateContract extends Source {
48-
deploy(contractData: ContractData, disableBundling?: boolean): Promise<ContractDeploy>;
49+
export interface CreateContract<T extends SerializationFormat> extends Source {
50+
deploy(contractData: ContractData<T>, disableBundling?: boolean): Promise<ContractDeploy>;
4951

50-
deployFromSourceTx(contractData: FromSrcTxContractData, disableBundling?: boolean): Promise<ContractDeploy>;
52+
deployFromSourceTx(contractData: FromSrcTxContractData<T>, disableBundling?: boolean): Promise<ContractDeploy>;
5153

5254
deployBundled(rawDataItem: Buffer): Promise<ContractDeploy>;
5355
}

src/contract/deploy/impl/DefaultCreateContract.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import { LoggerFactory } from '../../../logging/LoggerFactory';
99
import { CreateContract, ContractData, ContractDeploy, FromSrcTxContractData, ArWallet } from '../CreateContract';
1010
import { SourceData, SourceImpl } from './SourceImpl';
1111
import { Buffer } from 'redstone-isomorphic';
12+
import { SerializationFormat } from 'core/modules/StateEvaluator';
13+
import { exhaustive } from 'utils/utils';
1214

13-
export class DefaultCreateContract implements CreateContract {
15+
export class DefaultCreateContract<T extends SerializationFormat> implements CreateContract<T> {
1416
private readonly logger = LoggerFactory.INST.create('DefaultCreateContract');
1517
private readonly source: SourceImpl;
1618

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

24-
async deploy(contractData: ContractData, disableBundling?: boolean): Promise<ContractDeploy> {
25-
const { wallet, initState, tags, transfer, data } = contractData;
26+
async deploy<T extends SerializationFormat>(
27+
contractData: ContractData<T>,
28+
disableBundling?: boolean
29+
): Promise<ContractDeploy> {
30+
const { wallet, stateFormat, initState, tags, transfer, data } = contractData;
2631

2732
const effectiveUseBundler =
2833
disableBundling == undefined ? this.warp.definitionLoader.type() == 'warp' : !disableBundling;
@@ -38,6 +43,7 @@ export class DefaultCreateContract implements CreateContract {
3843
{
3944
srcTxId: srcTx.id,
4045
wallet,
46+
stateFormat,
4147
initState,
4248
tags,
4349
transfer,
@@ -48,13 +54,13 @@ export class DefaultCreateContract implements CreateContract {
4854
);
4955
}
5056

51-
async deployFromSourceTx(
52-
contractData: FromSrcTxContractData,
57+
async deployFromSourceTx<T extends SerializationFormat>(
58+
contractData: FromSrcTxContractData<T>,
5359
disableBundling?: boolean,
5460
srcTx: Transaction = null
5561
): Promise<ContractDeploy> {
5662
this.logger.debug('Creating new contract from src tx');
57-
const { wallet, srcTxId, initState, tags, transfer, data } = contractData;
63+
const { wallet, srcTxId, stateFormat, initState, tags, transfer, data } = contractData;
5864
this.signature = new Signature(this.warp, wallet);
5965
const signer = this.signature.signer;
6066

@@ -90,7 +96,28 @@ export class DefaultCreateContract implements CreateContract {
9096
typeof initState === 'string' ? initState : new TextDecoder().decode(initState)
9197
);
9298
} else {
93-
contractTX.addTag(SmartWeaveTags.CONTENT_TYPE, 'application/json');
99+
let contentType: undefined | string;
100+
101+
switch (stateFormat) {
102+
case SerializationFormat.JSON:
103+
contentType = 'application/json';
104+
break;
105+
case SerializationFormat.MSGPACK:
106+
// NOTE: There is still no officially registered Media Type for Messagepack and there are
107+
// apparently multiple different versions used in the wild like:
108+
// - application/msgpack
109+
// - application/x-msgpack
110+
// - application/x.msgpack
111+
// - [...]
112+
// See <https://github.com/msgpack/msgpack/issues/194>. I've decided to use the first one
113+
// as it looks like the one that makes the most sense.
114+
contentType = 'application/msgpack';
115+
break;
116+
default:
117+
return exhaustive(stateFormat);
118+
}
119+
120+
contractTX.addTag(SmartWeaveTags.CONTENT_TYPE, contentType);
94121
}
95122

96123
if (this.warp.environment === 'testnet') {

src/core/Warp.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { DefinitionLoader } from './modules/DefinitionLoader';
1616
import { ExecutorFactory } from './modules/ExecutorFactory';
1717
import { HandlerApi } from './modules/impl/HandlerExecutorFactory';
1818
import { InteractionsLoader } from './modules/InteractionsLoader';
19-
import { EvalStateResult, StateEvaluator } from './modules/StateEvaluator';
19+
import { EvalStateResult, SerializationFormat, StateEvaluator } from './modules/StateEvaluator';
2020
import { WarpBuilder } from './WarpBuilder';
2121
import { WarpPluginType, WarpPlugin, knownWarpPlugins } from './WarpPlugin';
2222
import { SortKeyCache } from '../cache/SortKeyCache';
@@ -39,7 +39,7 @@ export class Warp {
3939
/**
4040
* @deprecated createContract will be a private field, please use its methods directly e.g. await warp.deploy(...)
4141
*/
42-
readonly createContract: CreateContract;
42+
readonly createContract: CreateContract<SerializationFormat>;
4343
readonly testing: Testing;
4444

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

76-
async deploy(contractData: ContractData, disableBundling?: boolean): Promise<ContractDeploy> {
76+
async deploy<T extends SerializationFormat>(
77+
contractData: ContractData<T>,
78+
disableBundling?: boolean
79+
): Promise<ContractDeploy> {
7780
return await this.createContract.deploy(contractData, disableBundling);
7881
}
7982

80-
async deployFromSourceTx(contractData: FromSrcTxContractData, disableBundling?: boolean): Promise<ContractDeploy> {
83+
async deployFromSourceTx<T extends SerializationFormat>(
84+
contractData: FromSrcTxContractData<T>,
85+
disableBundling?: boolean
86+
): Promise<ContractDeploy> {
8187
return await this.createContract.deployFromSourceTx(contractData, disableBundling);
8288
}
8389

src/core/modules/StateEvaluator.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { pack, unpack } from 'msgpackr';
2+
import stringify from 'safe-stable-stringify';
3+
14
import { SortKeyCache, SortKeyCacheResult } from '../../cache/SortKeyCache';
25
import { CurrentTx } from '../../contract/Contract';
36
import { ExecutionContext } from '../../core/ExecutionContext';
@@ -138,8 +141,25 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
138141
throwOnInternalWriteError = true;
139142

140143
cacheEveryNInteractions = -1;
144+
145+
wasmSerializationFormat = SerializationFormat.JSON;
141146
}
142147

148+
export enum SerializationFormat {
149+
JSON = 'json',
150+
MSGPACK = 'msgpack'
151+
}
152+
153+
export const Serializers = {
154+
[SerializationFormat.JSON]: stringify,
155+
[SerializationFormat.MSGPACK]: pack
156+
} as const satisfies Record<SerializationFormat, unknown>;
157+
158+
export const Deserializers = {
159+
[SerializationFormat.JSON]: JSON.parse,
160+
[SerializationFormat.MSGPACK]: unpack
161+
} as const satisfies Record<SerializationFormat, unknown>;
162+
143163
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some features.
144164
export interface EvaluationOptions {
145165
// whether exceptions from given transaction interaction should be ignored
@@ -214,4 +234,11 @@ export interface EvaluationOptions {
214234
// force SDK to cache the state after evaluating each N interactions
215235
// defaults to -1, which effectively turns off this feature
216236
cacheEveryNInteractions: number;
237+
238+
/**
239+
* What serialization format should be used for the WASM<->JS bridge. Note that changing this
240+
* currently only affects Rust smartcontracts. AssemblyScript and Go smartcontracts will always
241+
* use JSON. Defaults to JSON.
242+
*/
243+
wasmSerializationFormat: SerializationFormat;
217244
}

src/core/modules/impl/CacheableStateEvaluator.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ExecutionContextModifier } from '../../../core/ExecutionContextModifier
66
import { GQLNodeInterface } from '../../../legacy/gqlResult';
77
import { LoggerFactory } from '../../../logging/LoggerFactory';
88
import { indent } from '../../../utils/utils';
9-
import { EvalStateResult } from '../StateEvaluator';
9+
import { EvalStateResult, SerializationFormat } from '../StateEvaluator';
1010
import { DefaultStateEvaluator } from './DefaultStateEvaluator';
1111
import { HandlerApi } from './HandlerExecutorFactory';
1212
import { genesisSortKey } from './LexicographicalInteractionsSorter';
@@ -35,11 +35,12 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
3535
currentTx: CurrentTx[]
3636
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
3737
const cachedState = executionContext.cachedState;
38+
const { wasmSerializationFormat: serializationFormat } = executionContext.evaluationOptions;
3839
if (cachedState && cachedState.sortKey == executionContext.requestedSortKey) {
3940
this.cLogger.info(
4041
`Exact cache hit for sortKey ${executionContext?.contractDefinition?.txId}:${cachedState.sortKey}`
4142
);
42-
executionContext.handler?.initState(cachedState.cachedValue.state);
43+
executionContext.handler?.initState(cachedState.cachedValue.state, serializationFormat);
4344
return cachedState;
4445
}
4546

@@ -71,10 +72,10 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
7172
if (missingInteractions.length == 0) {
7273
this.cLogger.info(`No missing interactions ${contractTxId}`);
7374
if (cachedState) {
74-
executionContext.handler?.initState(cachedState.cachedValue.state);
75+
executionContext.handler?.initState(cachedState.cachedValue.state, serializationFormat);
7576
return cachedState;
7677
} else {
77-
executionContext.handler?.initState(executionContext.contractDefinition.initState);
78+
executionContext.handler?.initState(executionContext.contractDefinition.initState, serializationFormat);
7879
this.cLogger.debug('Inserting initial state into cache');
7980
const stateToCache = new EvalStateResult(executionContext.contractDefinition.initState, {}, {});
8081
// no real sort-key - as we're returning the initial state

src/core/modules/impl/DefaultStateEvaluator.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,21 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
5353
executionContext: ExecutionContext<State, HandlerApi<State>>,
5454
currentTx: CurrentTx[]
5555
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
56-
const { ignoreExceptions, stackTrace, internalWrites, cacheEveryNInteractions } =
57-
executionContext.evaluationOptions;
56+
const {
57+
ignoreExceptions,
58+
stackTrace,
59+
internalWrites,
60+
cacheEveryNInteractions,
61+
wasmSerializationFormat: serializationFormat
62+
} = executionContext.evaluationOptions;
5863
const { contract, contractDefinition, sortedInteractions, warp } = executionContext;
5964

6065
let currentState = baseState.state;
6166
let currentSortKey = null;
6267
const validity = baseState.validity;
6368
const errorMessages = baseState.errorMessages;
6469

65-
executionContext?.handler.initState(currentState);
70+
executionContext?.handler.initState(currentState, serializationFormat);
6671

6772
const depth = executionContext.contract.callDepth();
6873

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

7883
const missingInteractionsLength = missingInteractions.length;
79-
executionContext.handler.initState(currentState);
84+
executionContext.handler.initState(currentState, serializationFormat);
8085

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

171176
validity[missingInteraction.id] = newState.cachedValue.validity[missingInteraction.id];
172177
if (newState.cachedValue.errorMessages?.[missingInteraction.id]) {

src/core/modules/impl/HandlerExecutorFactory.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import Arweave from 'arweave';
22
import loader from '@assemblyscript/loader';
33
import { asWasmImports } from './wasm/as-wasm-imports';
4-
import { rustWasmImports } from './wasm/rust-wasm-imports-msgpack';
4+
import { rustWasmImportsJson } from './wasm/rust-wasm-imports-json';
5+
import { rustWasmImportsMsgpack } from './wasm/rust-wasm-imports-msgpack';
56
import { Go } from './wasm/go-wasm-imports';
67
import * as vm2 from 'vm2';
78
import { WarpCache } from '../../../cache/WarpCache';
@@ -12,14 +13,14 @@ import { SmartWeaveGlobal } from '../../../legacy/smartweave-global';
1213
import { Benchmark } from '../../../logging/Benchmark';
1314
import { LoggerFactory } from '../../../logging/LoggerFactory';
1415
import { ExecutorFactory } from '../ExecutorFactory';
15-
import { EvalStateResult, EvaluationOptions } from '../StateEvaluator';
16+
import { EvalStateResult, EvaluationOptions, SerializationFormat } from '../StateEvaluator';
1617
import { JsHandlerApi } from './handler/JsHandlerApi';
1718
import { WasmHandlerApi } from './handler/WasmHandlerApi';
1819
import { normalizeContractSource } from './normalize-source';
1920
import { MemCache } from '../../../cache/impl/MemCache';
2021
import BigNumber from '../../../legacy/bignumber';
2122
import { Warp } from '../../Warp';
22-
import { isBrowser } from '../../../utils/utils';
23+
import { exhaustive, isBrowser } from '../../../utils/utils';
2324
import { Buffer } from 'redstone-isomorphic';
2425

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

109+
let rustWasmImports;
110+
switch (evaluationOptions.wasmSerializationFormat) {
111+
case SerializationFormat.JSON:
112+
rustWasmImports = rustWasmImportsJson;
113+
break;
114+
case SerializationFormat.MSGPACK:
115+
rustWasmImports = rustWasmImportsMsgpack;
116+
break;
117+
default:
118+
return exhaustive(evaluationOptions.wasmSerializationFormat);
119+
}
120+
108121
const { imports, exports } = rustWasmImports(
109122
swGlobal,
110123
wbindgenImports,
@@ -245,7 +258,7 @@ export interface HandlerApi<State> {
245258
interactionData: InteractionData<Input>
246259
): Promise<InteractionResult<State, Result>>;
247260

248-
initState(state: State): void;
261+
initState(state: State, format: SerializationFormat): void;
249262
}
250263

251264
export type HandlerFunction<State, Input, Result> = (

src/core/modules/impl/handler/AbstractContractHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ContractError, CurrentTx } from '../../../../contract/Contract';
22
import { ContractDefinition } from '../../../../core/ContractDefinition';
33
import { ExecutionContext } from '../../../../core/ExecutionContext';
4-
import { EvalStateResult } from '../../../../core/modules/StateEvaluator';
4+
import { EvalStateResult, SerializationFormat } from '../../../../core/modules/StateEvaluator';
55
import { GQLNodeInterface } from '../../../../legacy/gqlResult';
66
import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global';
77
import { LoggerFactory } from '../../../../logging/LoggerFactory';
@@ -27,7 +27,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
2727
interactionData: InteractionData<Input>
2828
): Promise<InteractionResult<State, Result>>;
2929

30-
abstract initState(state: State): void;
30+
abstract initState(state: State, format: SerializationFormat): void;
3131

3232
async dispose(): Promise<void> {
3333
// noop by default;

0 commit comments

Comments
 (0)