eth-compress 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/jit-compressor.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { LibZip } from 'solady';
2
2
 
3
- const MAX_160_BIT = (1n << 160n) - 1n;
3
+ const MAX_160_BIT = (1n << 128n) - 1n;
4
4
 
5
5
  const _normHex = (hex: string): string => hex.replace(/^0x/, '').toLowerCase();
6
6
 
@@ -70,7 +70,6 @@ const _jitDecompressor = function (calldata: string): string {
70
70
  let ops: number[] = [];
71
71
  let data: (number[] | null)[] = [];
72
72
  let stack: bigint[] = [];
73
- let stackFreq2 = new Map<bigint, number>();
74
73
  let trackedMemSize = 0;
75
74
  let mem = new Map<number, bigint>();
76
75
  const getStackIdx = (val: bigint): number => {
@@ -91,19 +90,20 @@ const _jitDecompressor = function (calldata: string): string {
91
90
  const pop2 = (): [bigint, bigint] => [stack.pop()!, stack.pop()!];
92
91
  const MASK32 = (1n << 256n) - 1n;
93
92
 
94
- const bump = <K>(m: Map<K, number>, k: K) => m.set(k, (m.get(k) || 0) + 1);
93
+ const ctr = <K>(m: Map<K, number>, k: K, delta: number) => m.set(k, (m.get(k) || 0) + delta);
94
+ const inc = <K>(m: Map<K, number>, k: K) => ctr(m, k, 1);
95
+ const dec = <K>(m: Map<K, number>, k: K) => ctr(m, k, -1);
95
96
  const pushOp = (op: number) => {
96
97
  ops.push(op);
97
- bump(opFreq, op);
98
+ inc(opFreq, op);
98
99
  };
99
100
  const pushD = (d: number[] | null) => {
100
101
  data.push(d || null);
101
- bump(dataFreq, d || null);
102
+ inc(dataFreq, d || null);
102
103
  };
103
- const pushS = (v: bigint) => {
104
+ const pushS = (v: bigint, freq: number = 1) => {
104
105
  stack.push(v);
105
- bump(stackFreq, v);
106
- bump(stackFreq2, v);
106
+ ctr(stackFreq, v, freq);
107
107
  ++pushCounter;
108
108
  stackCnt.set(v, pushCounter);
109
109
  };
@@ -113,40 +113,64 @@ const _jitDecompressor = function (calldata: string): string {
113
113
  };
114
114
 
115
115
  const addOp = (op: number, imm?: number[]) => {
116
- if (op === 0x59) {
117
- pushS(BigInt(trackedMemSize));
116
+ if (op === 0x36) {
117
+ pushS(32n, 0);
118
+ } else if (op === 0x59) {
119
+ pushS(BigInt(trackedMemSize), 0);
118
120
  } else if (op === 0x1b) {
119
- // SHL
120
- const [shift, val] = pop2();
121
- pushS((val << shift) & MASK32);
121
+ let [shift, val] = pop2();
122
+ if (ops[ops.length - 1] == 144) {
123
+ ops.pop();
124
+ data.pop();
125
+ [shift, val] = [val, shift];
126
+ }
127
+ pushS((val << BigInt(shift)) & MASK32);
122
128
  } else if (op === 0x17) {
123
129
  // OR
124
130
  const [a, b] = pop2();
131
+ if (ops[ops.length - 1] == 144) {
132
+ ops.pop();
133
+ data.pop();
134
+ }
125
135
  pushS((a | b) & MASK32);
126
136
  } else if ((op >= 0x60 && op <= 0x7f) || op === 0x5f) {
127
137
  // PUSH
128
138
  let v = 0n;
129
139
  for (const b of imm || []) v = (v << 8n) | BigInt(b);
130
- const idx = getStackIdx(v);
131
- pushS(v);
132
- if (idx !== -1 && op != 0x5f) {
133
- if (stackFreq2.get(v)! * 2 < stackFreq.get(v)!) {
134
- pushOp(128 + idx);
135
- pushD(null);
136
- }
137
- return;
138
- }
139
140
  if (v == 224n) {
140
141
  // Special‑case the literal 0xe0 (224):
141
142
  // the decompressor is always deployed at 0x...00e0, so the final
142
143
  // byte of ADDRESS is exactly 0xe0. Since we must send our own
143
- // address with the eth_call anyway, we can synthesize this value
144
+ // address with the eth_call anyway, we can synthesize this value
144
145
  // with a single opcode instead of encoding a literal, effectively
145
146
  // giving us one more hot constant slot on the stack.
147
+ pushS(v);
146
148
  pushOp(0x30); // ADDRESS
147
149
  pushD(null);
148
150
  return;
149
151
  }
152
+ const idx = getStackIdx(v);
153
+ if (idx !== -1 && op != 0x5f) {
154
+ const last = stackFreq.get(v) == 0;
155
+ if (idx == 0 && last) {
156
+ dec(stackFreq, v);
157
+ return;
158
+ }
159
+ if (idx == 1 && last) {
160
+ pushOp(144);
161
+ const [a, b] = pop2();
162
+ stack.push(b);
163
+ stack.push(a);
164
+ pushD(null);
165
+ dec(stackFreq, v);
166
+ return;
167
+ }
168
+ pushS(v, -1);
169
+ pushOp(128 + idx);
170
+ pushD(null);
171
+ return;
172
+ }
173
+ pushS(v);
150
174
  } else if (op === 0x51) {
151
175
  // MLOAD
152
176
  const k = Number(stack.pop()!);
@@ -168,10 +192,11 @@ const _jitDecompressor = function (calldata: string): string {
168
192
  pushOp(op);
169
193
  pushD(imm || null);
170
194
  };
171
-
195
+ const isInStack = (w) => stack.includes(w) || w == 0xe0 || w == 32n;
172
196
  const op = (opcode: number) => addOp(opcode);
173
197
  const pushN = (value: number | bigint) => {
174
198
  if (value > 0 && value === trackedMemSize) return addOp(0x59);
199
+ if (value == 32n) return addOp(0x36);
175
200
  if (!value) return addOp(0x5f, undefined); // PUSH0
176
201
  let v = BigInt(value);
177
202
  let bytes: number[] = [];
@@ -207,7 +232,7 @@ const _jitDecompressor = function (calldata: string): string {
207
232
  const emitPushN = (v: number | bigint) => (plan.push({ t: 'num', v }), pushN(v));
208
233
  const emitPushB = (b: Uint8Array) => (plan.push({ t: 'bytes', b }), pushB(b));
209
234
  const emitOp = (o: number) => (plan.push({ t: 'op', o }), op(o));
210
-
235
+ pushN(1n);
211
236
  // First pass: decide how to build each 32-byte word without emitting bytecode
212
237
  for (let base = 0; base < n; base += 32) {
213
238
  const word = new Uint8Array(32);
@@ -224,20 +249,11 @@ const _jitDecompressor = function (calldata: string): string {
224
249
 
225
250
  if (!seg.length) continue;
226
251
 
227
- const byte8s = seg.every(({ s, e }) => s === e);
228
- if (byte8s) {
229
- for (const { s } of seg) {
230
- emitPushN(word[s]);
231
- emitPushN(base + s);
232
- emitOp(0x53); // MSTORE8
233
- }
234
- continue;
235
- }
236
-
237
252
  // Decide whether to build this word via SHL/OR or as a single literal word
238
253
  const literal = word.slice(seg[0].s);
239
254
  const literalCost = 1 + literal.length;
240
-
255
+ let literalVal = 0n;
256
+ for (const b of literal) literalVal = (literalVal << 8n) | BigInt(b);
241
257
  const baseBytes = Math.ceil(Math.log2(base + 1) / 8);
242
258
  const wordHex = _uint8ArrayToHex(word);
243
259
  if (literalCost > 8) {
@@ -257,7 +273,19 @@ const _jitDecompressor = function (calldata: string): string {
257
273
  }
258
274
  }
259
275
 
260
- if (literalCost <= estShlCost(seg)) {
276
+ // Convert literal bytes to bigint for stack comparison
277
+
278
+ const byte8s = seg.every(({ s, e }) => s === e);
279
+ if (isInStack(literal)) {
280
+ emitPushB(literal);
281
+ } else if (byte8s) {
282
+ for (const { s } of seg) {
283
+ emitPushN(word[s]);
284
+ emitPushN(base + s);
285
+ emitOp(0x53); // MSTORE8
286
+ }
287
+ continue;
288
+ } else if (literalCost <= estShlCost(seg)) {
261
289
  emitPushB(literal);
262
290
  } else {
263
291
  let first = true;
@@ -281,20 +309,18 @@ const _jitDecompressor = function (calldata: string): string {
281
309
  stack = [];
282
310
  trackedMemSize = 0;
283
311
  mem = new Map();
284
-
285
312
  // Pre 2nd pass. Push most frequent literals into stack.
286
313
  Array.from(stackFreq.entries())
287
- .filter(([val, freq]) => freq > 1 && val !== 0n && val !== 224n)
314
+ .filter(([val, freq]) => freq > 1 && val > 1n && val !== 32n && val !== 224n)
315
+ .sort((a, b) => stackCnt.get(b[0])! - stackCnt.get(a[0])!)
288
316
  .filter(([val, _]) => {
289
- return typeof val === 'number' ? val : Number(val) <= MAX_160_BIT;
317
+ return typeof val === 'number' ? BigInt(val) : val <= MAX_160_BIT;
290
318
  })
291
- .sort((a, b) => stackCnt.get(b[0])! - stackCnt.get(a[0])!)
292
- .slice(0, 14)
319
+ .slice(0, 13)
293
320
  .forEach(([val, _]) => {
294
321
  pushN(val);
295
322
  });
296
-
297
- stackFreq2 = new Map();
323
+ pushN(1n);
298
324
  // Second pass: emit ops and track mem/stack
299
325
  for (const step of plan) {
300
326
  if (step.t === 'num') pushN(step.v);
@@ -312,7 +338,6 @@ const _jitDecompressor = function (calldata: string): string {
312
338
  // - 0x5f35: PUSH0 CALLDATALOAD (address from calldata[0])
313
339
  // - 0x5a: GAS (remaining gas)
314
340
  // - 0xf1: CALL
315
- // - 0x50: POP (discard success value)
316
341
  //
317
342
  // RETURNDATACOPY(destOffset=0, offset=0, length=RETURNDATASIZE):
318
343
  // - 0x3d5f5f3e: RETURNDATASIZE PUSH0 PUSH0 RETURNDATACOPY
@@ -334,10 +359,10 @@ const _jitDecompressor = function (calldata: string): string {
334
359
  // - CALLVALUE, load target address from calldata[0], GAS, CALL
335
360
  // - RETURNDATACOPY(0, 0, RETURNDATASIZE)
336
361
  // - RETURN(0, RETURNDATASIZE)
337
- return '0x' + _uint8ArrayToHex(new Uint8Array(out)) + '345f355af1503d5f5f3e3d5ff3';
362
+ return '0x' + _uint8ArrayToHex(new Uint8Array(out)) + '345f355af13d5f5f3e3d5ff3';
338
363
  };
339
364
 
340
- const MIN_SIZE_FOR_COMPRESSION = 800;
365
+ const MIN_SIZE_FOR_COMPRESSION = 1150;
341
366
  const DECOMPRESSOR_ADDRESS = '0x00000000000000000000000000000000000000e0';
342
367
 
343
368
  const _jit = 'jit';
@@ -357,19 +382,33 @@ export const compress_call = function (payload: any, alg?: string): any {
357
382
  const rpcMethod = payload.params?.[0]?.method || payload.method;
358
383
  if (rpcMethod && rpcMethod !== 'eth_call') return payload;
359
384
 
360
- const hex = _normHex(payload.data || '0x');
361
- const originalSize = (payload.data || '0x').length;
362
- if (originalSize < MIN_SIZE_FOR_COMPRESSION) return payload;
385
+ // Extract data and target address from params[0] if available, otherwise top-level
386
+ const txObj = payload.params?.[0] || payload;
387
+ const blockParam = payload.params?.[1] || 'latest';
388
+ const existingStateOverride = payload.params?.[2] || {};
389
+
390
+ // If there are any existing state overrides for the decompressor address, do not compress
391
+ const hex = _normHex(txObj.data || '0x');
392
+ const originalSize = (txObj.data || '0x').length;
393
+ if (originalSize < MIN_SIZE_FOR_COMPRESSION || (existingStateOverride[DECOMPRESSOR_ADDRESS])) return payload;
363
394
 
364
- const targetAddress = payload.to || '';
395
+ const targetAddress = txObj.to || '';
365
396
  const data = '0x' + hex;
366
397
 
367
398
  const autoSelect = !alg && originalSize < 1150;
368
399
  const flz = alg === _flz || autoSelect ? LibZip.flzCompress(data) : null;
369
400
  const cd = alg === _cd || autoSelect ? LibZip.cdCompress(data) : null;
370
-
371
- const selectedMethod =
372
- alg || (originalSize >= 1150 ? _jit : flz!.length < cd!.length ? _flz : _cd);
401
+ let selectedMethod = alg;
402
+ if (!selectedMethod) {
403
+ selectedMethod =
404
+ originalSize >= 1150 && (originalSize < 3000 || originalSize >= 8000)
405
+ ? _jit
406
+ : originalSize >= 3000 && originalSize < 8000
407
+ ? flz!.length < cd!.length
408
+ ? _flz
409
+ : _cd
410
+ : _cd;
411
+ }
373
412
 
374
413
  let bytecode: string;
375
414
  let calldata: string;
@@ -388,11 +427,17 @@ export const compress_call = function (payload: any, alg?: string): any {
388
427
 
389
428
  return {
390
429
  ...payload,
391
- to: DECOMPRESSOR_ADDRESS,
392
- data: calldata,
393
- stateDiff: {
394
- ...(payload.stateDiff || {}),
395
- [DECOMPRESSOR_ADDRESS]: { code: bytecode },
396
- },
430
+ params: [
431
+ {
432
+ ...txObj,
433
+ to: DECOMPRESSOR_ADDRESS,
434
+ data: calldata,
435
+ },
436
+ blockParam,
437
+ {
438
+ ...existingStateOverride,
439
+ [DECOMPRESSOR_ADDRESS]: { code: bytecode },
440
+ },
441
+ ],
397
442
  };
398
443
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eth-compress",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Client-to-server compression (viem-compatible) module for compressed, gas-efficient, low-latency eth_call requests.",
@@ -85,7 +85,8 @@
85
85
  "clean": "rm -rf dist *.tgz",
86
86
  "test:jit": "vitest run test/jit-compress.test.ts --config test/vitest.config.ts",
87
87
  "test:demo": "vitest run test/demo.test.ts --config test/vitest.config.ts",
88
- "test": "pnpm run build && vitest run --config test/vitest.config.ts",
88
+ "test:viem": "vitest run test/viem-multicall.test.ts --config test/vitest.config.ts",
89
+ "test": "pnpm run build && pnpm run test:jit && pnpm run test:demo && pnpm run test:viem",
89
90
  "lint": "biome lint .",
90
91
  "lint:fix": "biome lint --write .",
91
92
  "format": "biome format --write .",
@@ -98,7 +99,7 @@
98
99
  "node": ">=22",
99
100
  "pnpm": ">=10"
100
101
  },
101
- "packageManager": "pnpm@10.20.0",
102
+ "packageManager": "pnpm@10.22.0",
102
103
  "pnpm": {
103
104
  "peerDependencyRules": {
104
105
  "ignoreMissing": [
@@ -110,16 +111,16 @@
110
111
  "solady": "0.1.26"
111
112
  },
112
113
  "devDependencies": {
113
- "@biomejs/biome": "2.3.5",
114
- "@types/bun": "1.3.2",
115
- "@types/node": "24.10.1",
116
- "typescript": "5.9.3",
114
+ "@biomejs/biome": "2.3.6",
117
115
  "@ethereumjs/common": "10.1.0",
118
116
  "@ethereumjs/util": "10.1.0",
119
- "viem": "2.39.0",
120
117
  "@ethereumjs/vm": "10.1.0",
121
- "esbuild": "0.27.00",
122
- "vitest": "4.0.9"
118
+ "@types/bun": "1.3.2",
119
+ "@types/node": "24.10.1",
120
+ "esbuild": "0.27.0",
121
+ "typescript": "5.9.3",
122
+ "viem": "2.39.3",
123
+ "vitest": "4.0.10"
123
124
  },
124
125
  "browserslist": [
125
126
  ">0.3%",