From 352983e9002311c31d68952473d12a3c25b7b33c Mon Sep 17 00:00:00 2001
From: kiruse <26940742+Kiruse@users.noreply.github.com>
Date: Wed, 7 Dec 2022 19:22:14 +0100
Subject: [PATCH] fix copying raw bytes (ArrayBuffer & views); also introduce
NEVER_IMMUTIFY which prevents the storage system from converting specific
objects to Immutable collections
---
package.json | 2 +-
src/modules/wasm.ts | 8 ++++-
src/store/transactional.ts | 62 ++++++++++++++++++++++++++++++++------
src/types.ts | 2 ++
src/util.ts | 2 ++
5 files changed, 64 insertions(+), 12 deletions(-)
diff --git a/package.json b/package.json
index 0f53059..a49f198 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@terran-one/cw-simulate",
- "version": "2.7.1",
+ "version": "2.7.2",
"description": "Mock blockchain environment for simulating CosmWasm interactions",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
diff --git a/src/modules/wasm.ts b/src/modules/wasm.ts
index 623a6ba..50741a2 100644
--- a/src/modules/wasm.ts
+++ b/src/modules/wasm.ts
@@ -31,7 +31,7 @@ import {
import { Map } from 'immutable';
import { Err, Ok, Result } from 'ts-results';
import { fromBinary, fromRustResult, toBinary } from '../util';
-import { Transactional, TransactionalLens } from '../store/transactional';
+import { NEVER_IMMUTIFY, Transactional, TransactionalLens } from '../store/transactional';
function numberToBigEndianUint64(n: number): Uint8Array {
const buffer = new ArrayBuffer(8);
@@ -310,6 +310,7 @@ export class WasmModule {
if ('error' in response) {
let result = Err(response.error);
trace.push({
+ [NEVER_IMMUTIFY]: true,
type: 'instantiate' as 'instantiate',
contractAddress,
msg: instantiateMsg,
@@ -350,6 +351,7 @@ export class WasmModule {
);
trace.push({
+ [NEVER_IMMUTIFY]: true,
type: 'instantiate' as 'instantiate',
contractAddress,
msg: instantiateMsg,
@@ -420,6 +422,7 @@ export class WasmModule {
let result = Err(response.error);
trace.push({
+ [NEVER_IMMUTIFY]: true,
type: 'execute' as 'execute',
contractAddress,
msg: executeMsg,
@@ -462,6 +465,7 @@ export class WasmModule {
);
trace.push({
+ [NEVER_IMMUTIFY]: true,
type: 'execute' as 'execute',
contractAddress,
msg: executeMsg,
@@ -613,6 +617,7 @@ export class WasmModule {
let result = Err(response.error);
trace.push({
+ [NEVER_IMMUTIFY]: true,
type: 'reply' as 'reply',
contractAddress,
env: this.getExecutionEnv(contractAddress),
@@ -656,6 +661,7 @@ export class WasmModule {
);
trace.push({
+ [NEVER_IMMUTIFY]: true,
type: 'reply' as 'reply',
contractAddress,
msg: replyMsg,
diff --git a/src/store/transactional.ts b/src/store/transactional.ts
index 7fb15fd..4d0b01e 100644
--- a/src/store/transactional.ts
+++ b/src/store/transactional.ts
@@ -1,7 +1,13 @@
-import { fromJS, isCollection, isList, isMap, List, Map } from "immutable";
+import { isCollection, isList, isMap, List, Map } from "immutable";
import { Ok, Result } from "ts-results";
+import { isArrayLike } from "../util";
+
+// NEVER_IMMUTIFY is a string because that's easily serializable with different algorithms - symbols are not
+export type NeverImmutify = typeof NEVER_IMMUTIFY;
+export const NEVER_IMMUTIFY = '__NEVER_IMMUTIFY__';
type Primitive = boolean | number | bigint | string | null | undefined | symbol;
+type NoImmutify = Primitive | ArrayBuffer | ArrayBufferView | { [NEVER_IMMUTIFY]: any };
type Prefix
= [P, ...T];
type First = T extends Prefix ? F : never;
@@ -18,7 +24,7 @@ type Lens =
: T;
type Immutify =
- T extends Primitive
+ T extends NoImmutify
? T
: T extends ArrayLike
? List>
@@ -89,7 +95,7 @@ export class TransactionalLens {
initialize(data: M) {
this.db.tx(update => {
- const coll = fromJS(data);
+ const coll = toImmutable(data);
if (!isCollection(coll)) throw new Error('Not an Immutable.Map');
update(curr => curr.setIn([...this.prefix], coll));
return Ok(undefined);
@@ -112,8 +118,7 @@ export class TransactionalLens {
return this.db.tx(update => {
const setter: LensSetter = (...path: P) =>
(value: Lens | Immutify>) => {
- const v = isCollection(value) ? value : fromJS(value);
- update(curr => curr.setIn([...this.prefix, ...path], v));
+ update(curr => curr.setIn([...this.prefix, ...path], toImmutable(value)));
}
const deleter: LensDeleter = (...path: P) => {
update(curr => curr.deleteIn([...this.prefix, ...path]));
@@ -129,14 +134,52 @@ export class TransactionalLens {
get data() { return this.db.data.getIn([...this.prefix]) as Immutify }
}
+function toImmutable(value: any): any {
+ // passthru Immutable collections
+ if (isCollection(value)) return value;
+
+ // don't touch ArrayBuffers & ArrayBufferViews - freeze them
+ if (ArrayBuffer.isView(value)) {
+ Object.freeze(value.buffer);
+ return value;
+ }
+ if (value instanceof ArrayBuffer) {
+ Object.freeze(value);
+ return value;
+ }
+
+ // recurse into arrays & objects, converting them to lists & maps
+ // skip primitives & objects that don't want to be touched
+ if (typeof value === 'object' && !(NEVER_IMMUTIFY in value)) {
+ if (isArrayLike(value)) {
+ return List(value.map(item => toImmutable(item)));
+ } else {
+ return Map(
+ Object.entries(value).map(
+ ([key, value]) => [key, toImmutable(value)]
+ )
+ );
+ }
+ }
+
+ return value;
+}
+
function fromImmutable(value: any): any {
+ // reverse Immutable maps & lists
if (isMap(value)) {
return fromImmutable(value.toObject());
}
- else if (isList(value)) {
+ if (isList(value)) {
return fromImmutable(value.toArray());
}
- else if (typeof value === 'object') {
+
+ // passthru ArrayBuffers & ArrayBufferViews
+ if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return value;
+
+ // revert objects & arrays
+ // but: passthru objects w/ NEVER_IMMUTIFY
+ if (typeof value === 'object' && !(NEVER_IMMUTIFY in value)) {
if (typeof value.length === 'number' && 0 in value && value.length-1 in value) {
for (let i = 0; i < value.length; ++i) {
value[i] = fromImmutable(value[i]);
@@ -149,7 +192,6 @@ function fromImmutable(value: any): any {
}
return value;
}
- else {
- return value;
- }
+
+ return value;
}
diff --git a/src/types.ts b/src/types.ts
index 7955f0c..655a753 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,5 +1,6 @@
import Immutable from 'immutable';
import { Result } from 'ts-results';
+import type { NEVER_IMMUTIFY } from './store/transactional';
export interface ContractResponse {
messages: SubMsg[];
@@ -120,6 +121,7 @@ type CallDebugLog = {
export type Snapshot = Immutable.Map;
interface TraceLogCommon {
+ [NEVER_IMMUTIFY]: true;
type: string;
contractAddress: string;
env: ExecuteEnv;
diff --git a/src/util.ts b/src/util.ts
index ca0de4a..b7b23f9 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -2,6 +2,8 @@ import { fromBase64, fromUtf8, toBase64, toUtf8 } from "@cosmjs/encoding";
import { Err, Ok, Result } from "ts-results";
import { Binary, RustResult } from "./types";
+export const isArrayLike = (value: any): value is any[] => typeof value === 'object' && typeof value.length === 'number' && value[0] && value[value.length-1];
+
export const toBinary = (value: any): Binary => toBase64(toUtf8(JSON.stringify(value)));
export const fromBinary = (str: string): unknown => JSON.parse(fromUtf8(fromBase64(str)));