eth-compress 0.2.1 → 0.2.2
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 +29 -15
- 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 +4 -4
- package/_cjs/jit-compressor.cjs +4 -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 +4 -4
- package/_esm/jit-compressor.js +4 -5
- package/_esm/jit-compressor.js.map +3 -3
- package/_types/index.d.ts +1 -0
- package/_types/index.d.ts.map +1 -1
- package/_types/jit-compressor.d.ts +7 -20
- package/_types/jit-compressor.d.ts.map +1 -1
- package/index.node.ts +1 -1
- package/index.ts +6 -2
- package/jit-compressor.ts +266 -139
- package/package.json +6 -6
package/jit-compressor.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { LibZip } from 'solady';
|
|
2
|
+
import { MIN_BODY_SIZE } from './index';
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
+
const MAX_128_BIT = (1n << 128n) - 1n;
|
|
5
|
+
const MAX_256_BIT = (1n << 256n) - 1n;
|
|
4
6
|
|
|
5
7
|
const _normHex = (hex: string): string => hex.replace(/^0x/, '').toLowerCase();
|
|
6
8
|
|
|
@@ -22,6 +24,22 @@ const _uint8ArrayToHex = (bytes: Uint8Array): string => {
|
|
|
22
24
|
return hex;
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
const not = (a: bigint): bigint => ~a & MAX_256_BIT;
|
|
28
|
+
const and = (a: bigint, b: bigint): bigint => a & b & MAX_256_BIT;
|
|
29
|
+
const or = (a: bigint, b: bigint): bigint => (a | b) & MAX_256_BIT;
|
|
30
|
+
const xor = (a: bigint, b: bigint): bigint => (a ^ b) & MAX_256_BIT;
|
|
31
|
+
const shl = (shift: bigint, value: bigint): bigint => (value << shift) & MAX_256_BIT;
|
|
32
|
+
const shr = (shift: bigint, value: bigint): bigint => (value >> shift) & MAX_256_BIT;
|
|
33
|
+
const sub = (a: bigint, b: bigint): bigint => (a - b) & MAX_256_BIT;
|
|
34
|
+
const sigext = (byteSize: bigint, value: bigint): bigint => {
|
|
35
|
+
const numBytes = Number(byteSize) + 1;
|
|
36
|
+
const mask = (1n << BigInt(numBytes * 8)) - 1n;
|
|
37
|
+
const signBit = 1n << BigInt(numBytes * 8 - 1);
|
|
38
|
+
const maskedVal = value & mask;
|
|
39
|
+
const extended = maskedVal & signBit ? maskedVal | (~mask & MAX_256_BIT) : maskedVal;
|
|
40
|
+
return extended & MAX_256_BIT;
|
|
41
|
+
};
|
|
42
|
+
|
|
25
43
|
/**
|
|
26
44
|
* Generates FastLZ (LZ77) decompressor bytecode. The generated code decompresses incoming calldata and forwards it to the target address.
|
|
27
45
|
* @param address - Target contract address
|
|
@@ -29,7 +47,7 @@ const _uint8ArrayToHex = (bytes: Uint8Array): string => {
|
|
|
29
47
|
* @pure
|
|
30
48
|
*/
|
|
31
49
|
//! @__PURE__
|
|
32
|
-
|
|
50
|
+
const flzFwdBytecode = (address: string): string =>
|
|
33
51
|
`0x365f73${_normHex(address)}815b838110602f575f80848134865af1503d5f803e3d5ff35b803590815f1a8060051c908115609857600190600783149285831a6007018118840218600201948383011a90601f1660081b0101808603906020811860208211021890815f5b80830151818a015201858110609257505050600201019201916018565b82906075565b6001929350829150019101925f5b82811060b3575001916018565b85851060c1575b60010160a6565b936001818192355f1a878501530194905060ba56`;
|
|
34
52
|
|
|
35
53
|
/**
|
|
@@ -39,19 +57,9 @@ export const flzFwdBytecode = (address: string): string =>
|
|
|
39
57
|
* @pure
|
|
40
58
|
*/
|
|
41
59
|
//! @__PURE__
|
|
42
|
-
|
|
60
|
+
const rleFwdBytecode = (address: string): string =>
|
|
43
61
|
`0x5f5f5b368110602d575f8083813473${_normHex(address)}5af1503d5f803e3d5ff35b600180820192909160031981019035185f1a8015604c57815301906002565b505f19815282820192607f9060031981019035185f1a818111156072575b160101906002565b838101368437606a56`;
|
|
44
62
|
|
|
45
|
-
/**
|
|
46
|
-
* JIT Compiles decompressor bytecode
|
|
47
|
-
* @param calldata - Calldata to compress
|
|
48
|
-
* @pure
|
|
49
|
-
*/
|
|
50
|
-
//! @__PURE__
|
|
51
|
-
export const jitBytecode = function (calldata: string): string {
|
|
52
|
-
return _jitDecompressor('0x' + _normHex(calldata));
|
|
53
|
-
};
|
|
54
|
-
|
|
55
63
|
const _jitDecompressor = function (calldata: string): string {
|
|
56
64
|
const hex = _normHex(calldata);
|
|
57
65
|
const originalBuf = _hexToUint8Array(hex);
|
|
@@ -72,9 +80,11 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
72
80
|
let stack: bigint[] = [];
|
|
73
81
|
let trackedMemSize = 0;
|
|
74
82
|
let mem = new Map<number, bigint>();
|
|
83
|
+
let firstPass = true;
|
|
75
84
|
const getStackIdx = (val: bigint): number => {
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
let idx = stack.lastIndexOf(val);
|
|
86
|
+
idx = idx === -1 ? -1 : stack.length - 1 - idx;
|
|
87
|
+
return idx > 15 ? -1 : idx;
|
|
78
88
|
};
|
|
79
89
|
|
|
80
90
|
const opFreq = new Map<number, number>();
|
|
@@ -86,20 +96,15 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
86
96
|
|
|
87
97
|
let pushCounter = 0;
|
|
88
98
|
const stackCnt = new Map<bigint, number>();
|
|
89
|
-
|
|
90
99
|
const pop2 = (): [bigint, bigint] => [stack.pop()!, stack.pop()!];
|
|
91
|
-
const MASK32 = (1n << 256n) - 1n;
|
|
92
|
-
|
|
93
100
|
const ctr = <K>(m: Map<K, number>, k: K, delta: number) => m.set(k, (m.get(k) || 0) + delta);
|
|
94
101
|
const inc = <K>(m: Map<K, number>, k: K) => ctr(m, k, 1);
|
|
95
|
-
const
|
|
96
|
-
const pushOp = (op: number) => {
|
|
102
|
+
const pushOp = (op: number, d?: number[] | null) => {
|
|
97
103
|
ops.push(op);
|
|
98
104
|
inc(opFreq, op);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
inc(dataFreq, d || null);
|
|
105
|
+
const imm = d ?? null;
|
|
106
|
+
data.push(imm);
|
|
107
|
+
inc(dataFreq, imm);
|
|
103
108
|
};
|
|
104
109
|
const pushS = (v: bigint, freq: number = 1) => {
|
|
105
110
|
stack.push(v);
|
|
@@ -117,57 +122,68 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
117
122
|
pushS(32n, 0);
|
|
118
123
|
} else if (op === 0x59) {
|
|
119
124
|
pushS(BigInt(trackedMemSize), 0);
|
|
125
|
+
} else if (op === 0x0b) {
|
|
126
|
+
// SIGNEXTEND
|
|
127
|
+
const [byteSize, val] = pop2();
|
|
128
|
+
pushS(sigext(byteSize, val), 1);
|
|
129
|
+
} else if (op === 0x19) {
|
|
130
|
+
// NOT
|
|
131
|
+
const val = stack.pop()!;
|
|
132
|
+
pushS(not(val), 0);
|
|
133
|
+
} else if (op === 0x18) {
|
|
134
|
+
// XOR
|
|
135
|
+
const [a, b] = pop2();
|
|
136
|
+
pushS(xor(a, b), 1);
|
|
137
|
+
} else if (op === 0x16) {
|
|
138
|
+
// AND
|
|
139
|
+
const [a, b] = pop2();
|
|
140
|
+
pushS(and(a, b), 1);
|
|
141
|
+
} else if (op === 0x03) {
|
|
142
|
+
// SUB
|
|
143
|
+
const [a, b] = pop2();
|
|
144
|
+
pushS(sub(a, b), 1);
|
|
120
145
|
} else if (op === 0x1b) {
|
|
146
|
+
// SHL
|
|
121
147
|
let [shift, val] = pop2();
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
pushS((val << BigInt(shift)) & MASK32);
|
|
148
|
+
pushS(shl(shift, val), 1);
|
|
149
|
+
} else if (op === 0x1c) {
|
|
150
|
+
// SHR
|
|
151
|
+
let [shift, val] = pop2();
|
|
152
|
+
pushS(shr(shift, val), 1);
|
|
128
153
|
} else if (op === 0x17) {
|
|
129
154
|
// OR
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
ops.pop();
|
|
133
|
-
data.pop();
|
|
134
|
-
}
|
|
135
|
-
pushS((a | b) & MASK32);
|
|
155
|
+
let [a, b] = pop2();
|
|
156
|
+
pushS(or(a, b), 1);
|
|
136
157
|
} else if ((op >= 0x60 && op <= 0x7f) || op === 0x5f) {
|
|
137
158
|
// PUSH
|
|
138
159
|
let v = 0n;
|
|
139
160
|
for (const b of imm || []) v = (v << 8n) | BigInt(b);
|
|
140
161
|
if (v == 224n) {
|
|
141
|
-
|
|
142
|
-
// the decompressor is always deployed at 0x...00e0, so the final
|
|
143
|
-
// byte of ADDRESS is exactly 0xe0. Since we must send our own
|
|
144
|
-
// address with the eth_call anyway, we can synthesize this value
|
|
145
|
-
// with a single opcode instead of encoding a literal, effectively
|
|
146
|
-
// giving us one more hot constant slot on the stack.
|
|
147
|
-
pushS(v);
|
|
162
|
+
pushS(v, 0);
|
|
148
163
|
pushOp(0x30); // ADDRESS
|
|
149
|
-
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (v == 32n) {
|
|
167
|
+
pushS(v, 0);
|
|
168
|
+
pushOp(0x36); // ADDRESS
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (v === BigInt(trackedMemSize)) {
|
|
172
|
+
pushS(v, 0);
|
|
173
|
+
pushOp(0x59); // MemSize
|
|
150
174
|
return;
|
|
151
175
|
}
|
|
152
176
|
const idx = getStackIdx(v);
|
|
153
177
|
if (idx !== -1 && op != 0x5f) {
|
|
154
|
-
|
|
155
|
-
|
|
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);
|
|
178
|
+
let pushCtr = firstPass ? 1 : -1;
|
|
179
|
+
pushS(v, pushCtr);
|
|
169
180
|
pushOp(128 + idx);
|
|
170
|
-
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (v == MAX_256_BIT) {
|
|
184
|
+
pushS(v);
|
|
185
|
+
pushOp(0x5f); // 0
|
|
186
|
+
pushOp(0x19); // NOT
|
|
171
187
|
return;
|
|
172
188
|
}
|
|
173
189
|
pushS(v);
|
|
@@ -179,7 +195,7 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
179
195
|
// MSTORE
|
|
180
196
|
const [offset, value] = pop2();
|
|
181
197
|
const k = Number(offset);
|
|
182
|
-
mem.set(k, value &
|
|
198
|
+
mem.set(k, value & MAX_256_BIT);
|
|
183
199
|
trackMem(k, 32);
|
|
184
200
|
} else if (op === 0x53) {
|
|
185
201
|
// MSTORE8
|
|
@@ -189,10 +205,8 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
189
205
|
// RETURN
|
|
190
206
|
pop2();
|
|
191
207
|
}
|
|
192
|
-
pushOp(op);
|
|
193
|
-
pushD(imm || null);
|
|
208
|
+
pushOp(op, imm || null);
|
|
194
209
|
};
|
|
195
|
-
const isInStack = (w) => stack.includes(w) || w == 0xe0 || w == 32n;
|
|
196
210
|
const op = (opcode: number) => addOp(opcode);
|
|
197
211
|
const pushN = (value: number | bigint) => {
|
|
198
212
|
if (value > 0 && value === trackedMemSize) return addOp(0x59);
|
|
@@ -234,6 +248,7 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
234
248
|
const emitOp = (o: number) => (plan.push({ t: 'op', o }), op(o));
|
|
235
249
|
pushN(1n);
|
|
236
250
|
// First pass: decide how to build each 32-byte word without emitting bytecode
|
|
251
|
+
const _stack = [1n, 32n, 224n];
|
|
237
252
|
for (let base = 0; base < n; base += 32) {
|
|
238
253
|
const word = new Uint8Array(32);
|
|
239
254
|
word.set(buf.slice(base, Math.min(base + 32, n)), 0);
|
|
@@ -256,6 +271,15 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
256
271
|
for (const b of literal) literalVal = (literalVal << 8n) | BigInt(b);
|
|
257
272
|
const baseBytes = Math.ceil(Math.log2(base + 1) / 8);
|
|
258
273
|
const wordHex = _uint8ArrayToHex(word);
|
|
274
|
+
const shlCost = estShlCost(seg);
|
|
275
|
+
|
|
276
|
+
const inStack = _stack.includes(literalVal);
|
|
277
|
+
if (inStack) {
|
|
278
|
+
emitPushB(literal);
|
|
279
|
+
emitPushN(base);
|
|
280
|
+
emitOp(0x52); // MSTORE
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
259
283
|
if (literalCost > 8) {
|
|
260
284
|
if (wordCache.has(wordHex)) {
|
|
261
285
|
if (literalCost > wordCacheCost.get(wordHex)! + baseBytes) {
|
|
@@ -273,31 +297,134 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
273
297
|
}
|
|
274
298
|
}
|
|
275
299
|
|
|
276
|
-
// Convert literal bytes to bigint for stack comparison
|
|
277
|
-
|
|
278
300
|
const byte8s = seg.every(({ s, e }) => s === e);
|
|
279
|
-
|
|
280
|
-
|
|
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)) {
|
|
301
|
+
const byte8sCost = seg.length * 3; // each: PUSH1 (value), PUSH1 (offset), MSTORE8
|
|
302
|
+
if (inStack) {
|
|
289
303
|
emitPushB(literal);
|
|
290
304
|
} else {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
emitPushB(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
305
|
+
// Aggregate all costs
|
|
306
|
+
let bestCost = literalCost;
|
|
307
|
+
let bestEmit: (() => void) | null = () => {
|
|
308
|
+
emitPushB(literal);
|
|
309
|
+
};
|
|
310
|
+
if (literalVal == MAX_256_BIT) {
|
|
311
|
+
bestCost = 2;
|
|
312
|
+
bestEmit = () => {
|
|
313
|
+
emitPushN(notVal);
|
|
314
|
+
emitOp(0x19);
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// Try NOT: PUSH<n> ~val, NOT
|
|
318
|
+
const notVal = not(literalVal);
|
|
319
|
+
let notBytes = 0;
|
|
320
|
+
let tmp = notVal;
|
|
321
|
+
while (tmp > 0n) {
|
|
322
|
+
++notBytes;
|
|
323
|
+
tmp >>= 8n;
|
|
324
|
+
}
|
|
325
|
+
notBytes = 1 + notBytes;
|
|
326
|
+
if (notBytes === 0) notBytes;
|
|
327
|
+
const notCost = notBytes + 1; // PUSH<n> + NOT
|
|
328
|
+
if (notCost < bestCost) {
|
|
329
|
+
bestCost = notCost;
|
|
330
|
+
bestEmit = () => {
|
|
331
|
+
emitPushN(notVal);
|
|
332
|
+
emitOp(0x19);
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Try SUB: PUSH1 0, PUSH<n> val, SUB
|
|
337
|
+
const subVal = sub(0n, literalVal);
|
|
338
|
+
let subBytes = 0;
|
|
339
|
+
tmp = subVal;
|
|
340
|
+
while (tmp > 0n) {
|
|
341
|
+
++subBytes;
|
|
342
|
+
tmp >>= 8n;
|
|
343
|
+
}
|
|
344
|
+
if (subBytes === 0) subBytes = 1;
|
|
345
|
+
if (_stack.includes(subVal)) subBytes = 1;
|
|
346
|
+
const subCost = 1 + (1 + subBytes) + 1; // PUSH0 + PUSH<n> + SUB
|
|
347
|
+
if (subCost < bestCost) {
|
|
348
|
+
bestCost = subCost;
|
|
349
|
+
bestEmit = () => {
|
|
350
|
+
emitPushN(0);
|
|
351
|
+
emitPushN(subVal);
|
|
352
|
+
emitOp(0x03);
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Try SIGNEXTEND: PUSH<n> truncated, PUSH1 byteSize, SIGNEXTEND
|
|
357
|
+
for (let numBytes = 1; numBytes < literal.length; numBytes++) {
|
|
358
|
+
const mask = (1n << BigInt(numBytes * 8)) - 1n;
|
|
359
|
+
const truncated = literalVal & mask;
|
|
360
|
+
const extended = sigext(BigInt(numBytes - 1), truncated);
|
|
361
|
+
if (extended === literalVal && (truncated & (1n << BigInt(numBytes * 8 - 1))) !== 0n) {
|
|
362
|
+
let trueByteCost = 1 + numBytes;
|
|
363
|
+
if (_stack.includes(BigInt(extended))) trueByteCost = 1;
|
|
364
|
+
let signCost = trueByteCost + (1 + 1) + 1; // PUSH<n> + PUSH1 + SIGNEXTEND
|
|
365
|
+
if (signCost < bestCost) {
|
|
366
|
+
bestCost = signCost;
|
|
367
|
+
bestEmit = () => {
|
|
368
|
+
emitPushN(truncated);
|
|
369
|
+
emitPushN(numBytes - 1);
|
|
370
|
+
emitOp(0x0b);
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
298
374
|
}
|
|
299
|
-
|
|
300
|
-
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Try SHIFT+NOT: PUSH<n> val, PUSH1 shift, SHL, NOT
|
|
378
|
+
for (let shiftBits = 8; shiftBits <= 248; shiftBits += 8) {
|
|
379
|
+
const shifted = shr(BigInt(shiftBits), literalVal);
|
|
380
|
+
if (shifted === 0n) break;
|
|
381
|
+
|
|
382
|
+
const notShifted = not(shifted);
|
|
383
|
+
const reconstructed = shl(BigInt(shiftBits), notShifted);
|
|
384
|
+
|
|
385
|
+
if (reconstructed === literalVal) {
|
|
386
|
+
let shiftedBytes = 0;
|
|
387
|
+
let tmpShifted = notShifted;
|
|
388
|
+
while (tmpShifted > 0n) {
|
|
389
|
+
++shiftedBytes;
|
|
390
|
+
tmpShifted >>= 8n;
|
|
391
|
+
}
|
|
392
|
+
if (shiftedBytes === 0) shiftedBytes = 1;
|
|
393
|
+
const shiftNotCost = 1 + shiftedBytes + 2 + 1 + 1; // PUSH<n> + PUSH1 + SHL + NOT
|
|
394
|
+
if (shiftNotCost < bestCost) {
|
|
395
|
+
bestCost = shiftNotCost;
|
|
396
|
+
bestEmit = () => {
|
|
397
|
+
emitPushN(notShifted);
|
|
398
|
+
emitPushN(shiftBits);
|
|
399
|
+
emitOp(0x1b); // SHL
|
|
400
|
+
emitOp(0x19); // NOT
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (byte8s && byte8sCost < bestCost && byte8sCost <= shlCost) {
|
|
407
|
+
for (const { s } of seg) {
|
|
408
|
+
emitPushN(word[s]);
|
|
409
|
+
emitPushN(base + s);
|
|
410
|
+
emitOp(0x53); // MSTORE8
|
|
411
|
+
}
|
|
412
|
+
continue; // Skip the single MSTORE at the end
|
|
413
|
+
} else if (shlCost < bestCost) {
|
|
414
|
+
// Use SHL/OR
|
|
415
|
+
let first = true;
|
|
416
|
+
for (const { s, e } of seg) {
|
|
417
|
+
const suffix0s = 31 - e;
|
|
418
|
+
emitPushB(word.slice(s, e + 1));
|
|
419
|
+
if (suffix0s > 0) {
|
|
420
|
+
emitPushN(suffix0s * 8);
|
|
421
|
+
emitOp(0x1b); // SHL
|
|
422
|
+
}
|
|
423
|
+
if (!first) emitOp(0x17); // OR
|
|
424
|
+
first = false;
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
bestEmit!();
|
|
301
428
|
}
|
|
302
429
|
}
|
|
303
430
|
emitPushN(base);
|
|
@@ -314,9 +441,9 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
314
441
|
.filter(([val, freq]) => freq > 1 && val > 1n && val !== 32n && val !== 224n)
|
|
315
442
|
.sort((a, b) => stackCnt.get(b[0])! - stackCnt.get(a[0])!)
|
|
316
443
|
.filter(([val, _]) => {
|
|
317
|
-
return typeof val === 'number' ? BigInt(val) : val <=
|
|
444
|
+
return typeof val === 'number' ? BigInt(val) : val <= MAX_128_BIT;
|
|
318
445
|
})
|
|
319
|
-
.slice(0,
|
|
446
|
+
.slice(0, 15)
|
|
320
447
|
.forEach(([val, _]) => {
|
|
321
448
|
pushN(val);
|
|
322
449
|
});
|
|
@@ -362,16 +489,18 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
362
489
|
return '0x' + _uint8ArrayToHex(new Uint8Array(out)) + '345f355af13d5f5f3e3d5ff3';
|
|
363
490
|
};
|
|
364
491
|
|
|
365
|
-
const MIN_SIZE_FOR_COMPRESSION = 1150;
|
|
366
492
|
const DECOMPRESSOR_ADDRESS = '0x00000000000000000000000000000000000000e0';
|
|
367
493
|
|
|
368
|
-
const _jit = 'jit';
|
|
369
|
-
const _flz = 'flz';
|
|
370
|
-
const _cd = 'cd';
|
|
371
|
-
|
|
372
494
|
/**
|
|
373
495
|
* Compresses eth_call payload using JIT, FastLZ (FLZ), or calldata RLE (CD) compression.
|
|
374
496
|
* Auto-selects best algorithm if not specified. Only compresses if >800 bytes and beneficial.
|
|
497
|
+
*
|
|
498
|
+
* Only applies compression to calls that:
|
|
499
|
+
* - target the latest block ID
|
|
500
|
+
* - have no state overrides
|
|
501
|
+
* - have a target address and calldata
|
|
502
|
+
* - have no other properties (nonce, gas, etc.)
|
|
503
|
+
*
|
|
375
504
|
* @param payload - eth_call RPC payload
|
|
376
505
|
* @param alg - 'jit' | 'flz' | 'cd' | undefined (auto)
|
|
377
506
|
* @returns (un)compressed eth_call payload
|
|
@@ -379,65 +508,63 @@ const _cd = 'cd';
|
|
|
379
508
|
*/
|
|
380
509
|
//! @__PURE__
|
|
381
510
|
export const compress_call = function (payload: any, alg?: string): any {
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const autoSelect = !alg && originalSize < 1150;
|
|
399
|
-
const flz = alg === _flz || autoSelect ? LibZip.flzCompress(data) : null;
|
|
400
|
-
const cd = alg === _cd || autoSelect ? LibZip.cdCompress(data) : null;
|
|
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;
|
|
511
|
+
const { method, params } = payload;
|
|
512
|
+
if (method && method !== 'eth_call') return payload;
|
|
513
|
+
const txObj = params?.[0] || payload;
|
|
514
|
+
const blockParam = params?.[1];
|
|
515
|
+
const overrides = params?.[2];
|
|
516
|
+
|
|
517
|
+
// Validation
|
|
518
|
+
if (
|
|
519
|
+
(blockParam && blockParam !== 'latest') ||
|
|
520
|
+
(overrides && Object.keys(overrides).length > 0) ||
|
|
521
|
+
!txObj?.to ||
|
|
522
|
+
!txObj?.data ||
|
|
523
|
+
Object.keys(txObj).some((k) => !['to', 'data', 'from'].includes(k))
|
|
524
|
+
) {
|
|
525
|
+
return payload;
|
|
411
526
|
}
|
|
412
527
|
|
|
528
|
+
const originalSize = txObj.data.length;
|
|
529
|
+
if (originalSize < MIN_BODY_SIZE) return payload;
|
|
530
|
+
|
|
531
|
+
const inputData = '0x' + _normHex(txObj.data);
|
|
532
|
+
const to = txObj.to;
|
|
533
|
+
|
|
534
|
+
// Determine compression method and generate bytecode/calldata
|
|
413
535
|
let bytecode: string;
|
|
414
536
|
let calldata: string;
|
|
415
537
|
|
|
416
|
-
if (
|
|
417
|
-
bytecode = _jitDecompressor(
|
|
418
|
-
calldata = '0x' + _normHex(
|
|
538
|
+
if (alg === 'jit' || (!alg && (originalSize < 3000 || originalSize >= 8000))) {
|
|
539
|
+
bytecode = _jitDecompressor(inputData);
|
|
540
|
+
calldata = '0x' + _normHex(to).padStart(64, '0');
|
|
419
541
|
} else {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
542
|
+
// Need FLZ and/or CD compression
|
|
543
|
+
const flzData = alg === 'flz' || !alg ? LibZip.flzCompress(inputData) : null;
|
|
544
|
+
const cdData = alg === 'cd' || (!alg && flzData) ? LibZip.cdCompress(inputData) : null;
|
|
545
|
+
|
|
546
|
+
// Pick the best or requested one
|
|
547
|
+
const useFlz =
|
|
548
|
+
alg === 'flz' || (!alg && flzData && (!cdData || flzData.length < cdData.length));
|
|
549
|
+
|
|
550
|
+
if (useFlz) {
|
|
551
|
+
calldata = flzData!;
|
|
552
|
+
bytecode = flzFwdBytecode(to);
|
|
553
|
+
} else {
|
|
554
|
+
calldata = cdData!;
|
|
555
|
+
bytecode = rleFwdBytecode(to);
|
|
556
|
+
}
|
|
423
557
|
}
|
|
424
558
|
|
|
425
|
-
|
|
426
|
-
if (
|
|
559
|
+
// Skip if not beneficial
|
|
560
|
+
if (bytecode.length + calldata.length >= originalSize) return payload;
|
|
427
561
|
|
|
428
562
|
return {
|
|
429
563
|
...payload,
|
|
430
564
|
params: [
|
|
431
|
-
{
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
data: calldata,
|
|
435
|
-
},
|
|
436
|
-
blockParam,
|
|
437
|
-
{
|
|
438
|
-
...existingStateOverride,
|
|
439
|
-
[DECOMPRESSOR_ADDRESS]: { code: bytecode },
|
|
440
|
-
},
|
|
565
|
+
{ ...txObj, to: DECOMPRESSOR_ADDRESS, data: calldata },
|
|
566
|
+
blockParam || 'latest',
|
|
567
|
+
{ ...overrides, [DECOMPRESSOR_ADDRESS]: { code: bytecode } },
|
|
441
568
|
],
|
|
442
569
|
};
|
|
443
570
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eth-compress",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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.",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"node": ">=22",
|
|
100
100
|
"pnpm": ">=10"
|
|
101
101
|
},
|
|
102
|
-
"packageManager": "pnpm@10.
|
|
102
|
+
"packageManager": "pnpm@10.23.0",
|
|
103
103
|
"pnpm": {
|
|
104
104
|
"peerDependencyRules": {
|
|
105
105
|
"ignoreMissing": [
|
|
@@ -111,16 +111,16 @@
|
|
|
111
111
|
"solady": "0.1.26"
|
|
112
112
|
},
|
|
113
113
|
"devDependencies": {
|
|
114
|
-
"@biomejs/biome": "2.3.
|
|
114
|
+
"@biomejs/biome": "2.3.7",
|
|
115
115
|
"@ethereumjs/common": "10.1.0",
|
|
116
116
|
"@ethereumjs/util": "10.1.0",
|
|
117
117
|
"@ethereumjs/vm": "10.1.0",
|
|
118
|
-
"@types/bun": "1.3.
|
|
118
|
+
"@types/bun": "1.3.3",
|
|
119
119
|
"@types/node": "24.10.1",
|
|
120
120
|
"esbuild": "0.27.0",
|
|
121
121
|
"typescript": "5.9.3",
|
|
122
|
-
"viem": "2.
|
|
123
|
-
"vitest": "4.0.
|
|
122
|
+
"viem": "2.40.2",
|
|
123
|
+
"vitest": "4.0.14"
|
|
124
124
|
},
|
|
125
125
|
"browserslist": [
|
|
126
126
|
">0.3%",
|