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/README.md +6 -6
- package/_cjs/index.cjs +1 -1
- package/_cjs/index.cjs.map +3 -3
- package/_cjs/index.node.cjs +1 -1
- package/_cjs/index.node.cjs.map +3 -3
- package/_cjs/jit-compressor.cjs +5 -5
- package/_cjs/jit-compressor.cjs.map +3 -3
- package/_esm/index.js +1 -1
- package/_esm/index.js.map +3 -3
- package/_esm/index.node.js +1 -1
- package/_esm/index.node.js.map +3 -3
- package/_esm/jit-compressor.js +5 -5
- package/_esm/jit-compressor.js.map +3 -3
- package/_types/jit-compressor.d.ts.map +1 -1
- package/jit-compressor.ts +105 -60
- package/package.json +11 -10
package/jit-compressor.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LibZip } from 'solady';
|
|
2
2
|
|
|
3
|
-
const MAX_160_BIT = (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
|
|
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
|
-
|
|
98
|
+
inc(opFreq, op);
|
|
98
99
|
};
|
|
99
100
|
const pushD = (d: number[] | null) => {
|
|
100
101
|
data.push(d || null);
|
|
101
|
-
|
|
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
|
-
|
|
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 ===
|
|
117
|
-
pushS(
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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 !==
|
|
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 :
|
|
317
|
+
return typeof val === 'number' ? BigInt(val) : val <= MAX_160_BIT;
|
|
290
318
|
})
|
|
291
|
-
.
|
|
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)) + '
|
|
362
|
+
return '0x' + _uint8ArrayToHex(new Uint8Array(out)) + '345f355af13d5f5f3e3d5ff3';
|
|
338
363
|
};
|
|
339
364
|
|
|
340
|
-
const MIN_SIZE_FOR_COMPRESSION =
|
|
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
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
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 =
|
|
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
|
-
|
|
372
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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.
|
|
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": "
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
122
|
-
"
|
|
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%",
|