eth-compress 0.2.0 → 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 +31 -17
- 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 +289 -117
- package/package.json +11 -10
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);
|
|
@@ -70,12 +78,13 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
70
78
|
let ops: number[] = [];
|
|
71
79
|
let data: (number[] | null)[] = [];
|
|
72
80
|
let stack: bigint[] = [];
|
|
73
|
-
let stackFreq2 = new Map<bigint, number>();
|
|
74
81
|
let trackedMemSize = 0;
|
|
75
82
|
let mem = new Map<number, bigint>();
|
|
83
|
+
let firstPass = true;
|
|
76
84
|
const getStackIdx = (val: bigint): number => {
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
let idx = stack.lastIndexOf(val);
|
|
86
|
+
idx = idx === -1 ? -1 : stack.length - 1 - idx;
|
|
87
|
+
return idx > 15 ? -1 : idx;
|
|
79
88
|
};
|
|
80
89
|
|
|
81
90
|
const opFreq = new Map<number, number>();
|
|
@@ -87,23 +96,19 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
87
96
|
|
|
88
97
|
let pushCounter = 0;
|
|
89
98
|
const stackCnt = new Map<bigint, number>();
|
|
90
|
-
|
|
91
99
|
const pop2 = (): [bigint, bigint] => [stack.pop()!, stack.pop()!];
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const pushOp = (op: number) => {
|
|
100
|
+
const ctr = <K>(m: Map<K, number>, k: K, delta: number) => m.set(k, (m.get(k) || 0) + delta);
|
|
101
|
+
const inc = <K>(m: Map<K, number>, k: K) => ctr(m, k, 1);
|
|
102
|
+
const pushOp = (op: number, d?: number[] | null) => {
|
|
96
103
|
ops.push(op);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
bump(dataFreq, d || null);
|
|
104
|
+
inc(opFreq, op);
|
|
105
|
+
const imm = d ?? null;
|
|
106
|
+
data.push(imm);
|
|
107
|
+
inc(dataFreq, imm);
|
|
102
108
|
};
|
|
103
|
-
const pushS = (v: bigint) => {
|
|
109
|
+
const pushS = (v: bigint, freq: number = 1) => {
|
|
104
110
|
stack.push(v);
|
|
105
|
-
|
|
106
|
-
bump(stackFreq2, v);
|
|
111
|
+
ctr(stackFreq, v, freq);
|
|
107
112
|
++pushCounter;
|
|
108
113
|
stackCnt.set(v, pushCounter);
|
|
109
114
|
};
|
|
@@ -113,40 +118,75 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
113
118
|
};
|
|
114
119
|
|
|
115
120
|
const addOp = (op: number, imm?: number[]) => {
|
|
116
|
-
if (op ===
|
|
117
|
-
pushS(
|
|
121
|
+
if (op === 0x36) {
|
|
122
|
+
pushS(32n, 0);
|
|
123
|
+
} else if (op === 0x59) {
|
|
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);
|
|
118
145
|
} else if (op === 0x1b) {
|
|
119
146
|
// SHL
|
|
120
|
-
|
|
121
|
-
pushS((val
|
|
147
|
+
let [shift, val] = pop2();
|
|
148
|
+
pushS(shl(shift, val), 1);
|
|
149
|
+
} else if (op === 0x1c) {
|
|
150
|
+
// SHR
|
|
151
|
+
let [shift, val] = pop2();
|
|
152
|
+
pushS(shr(shift, val), 1);
|
|
122
153
|
} else if (op === 0x17) {
|
|
123
154
|
// OR
|
|
124
|
-
|
|
125
|
-
pushS((a
|
|
155
|
+
let [a, b] = pop2();
|
|
156
|
+
pushS(or(a, b), 1);
|
|
126
157
|
} else if ((op >= 0x60 && op <= 0x7f) || op === 0x5f) {
|
|
127
158
|
// PUSH
|
|
128
159
|
let v = 0n;
|
|
129
160
|
for (const b of imm || []) v = (v << 8n) | BigInt(b);
|
|
161
|
+
if (v == 224n) {
|
|
162
|
+
pushS(v, 0);
|
|
163
|
+
pushOp(0x30); // ADDRESS
|
|
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
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
130
176
|
const idx = getStackIdx(v);
|
|
131
|
-
pushS(v);
|
|
132
177
|
if (idx !== -1 && op != 0x5f) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
178
|
+
let pushCtr = firstPass ? 1 : -1;
|
|
179
|
+
pushS(v, pushCtr);
|
|
180
|
+
pushOp(128 + idx);
|
|
137
181
|
return;
|
|
138
182
|
}
|
|
139
|
-
if (v ==
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
// address with the eth_call anyway, we can synthesize this value
|
|
144
|
-
// with a single opcode instead of encoding a literal, effectively
|
|
145
|
-
// giving us one more hot constant slot on the stack.
|
|
146
|
-
pushOp(0x30); // ADDRESS
|
|
147
|
-
pushD(null);
|
|
183
|
+
if (v == MAX_256_BIT) {
|
|
184
|
+
pushS(v);
|
|
185
|
+
pushOp(0x5f); // 0
|
|
186
|
+
pushOp(0x19); // NOT
|
|
148
187
|
return;
|
|
149
188
|
}
|
|
189
|
+
pushS(v);
|
|
150
190
|
} else if (op === 0x51) {
|
|
151
191
|
// MLOAD
|
|
152
192
|
const k = Number(stack.pop()!);
|
|
@@ -155,7 +195,7 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
155
195
|
// MSTORE
|
|
156
196
|
const [offset, value] = pop2();
|
|
157
197
|
const k = Number(offset);
|
|
158
|
-
mem.set(k, value &
|
|
198
|
+
mem.set(k, value & MAX_256_BIT);
|
|
159
199
|
trackMem(k, 32);
|
|
160
200
|
} else if (op === 0x53) {
|
|
161
201
|
// MSTORE8
|
|
@@ -165,13 +205,12 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
165
205
|
// RETURN
|
|
166
206
|
pop2();
|
|
167
207
|
}
|
|
168
|
-
pushOp(op);
|
|
169
|
-
pushD(imm || null);
|
|
208
|
+
pushOp(op, imm || null);
|
|
170
209
|
};
|
|
171
|
-
|
|
172
210
|
const op = (opcode: number) => addOp(opcode);
|
|
173
211
|
const pushN = (value: number | bigint) => {
|
|
174
212
|
if (value > 0 && value === trackedMemSize) return addOp(0x59);
|
|
213
|
+
if (value == 32n) return addOp(0x36);
|
|
175
214
|
if (!value) return addOp(0x5f, undefined); // PUSH0
|
|
176
215
|
let v = BigInt(value);
|
|
177
216
|
let bytes: number[] = [];
|
|
@@ -207,8 +246,9 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
207
246
|
const emitPushN = (v: number | bigint) => (plan.push({ t: 'num', v }), pushN(v));
|
|
208
247
|
const emitPushB = (b: Uint8Array) => (plan.push({ t: 'bytes', b }), pushB(b));
|
|
209
248
|
const emitOp = (o: number) => (plan.push({ t: 'op', o }), op(o));
|
|
210
|
-
|
|
249
|
+
pushN(1n);
|
|
211
250
|
// First pass: decide how to build each 32-byte word without emitting bytecode
|
|
251
|
+
const _stack = [1n, 32n, 224n];
|
|
212
252
|
for (let base = 0; base < n; base += 32) {
|
|
213
253
|
const word = new Uint8Array(32);
|
|
214
254
|
word.set(buf.slice(base, Math.min(base + 32, n)), 0);
|
|
@@ -224,22 +264,22 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
224
264
|
|
|
225
265
|
if (!seg.length) continue;
|
|
226
266
|
|
|
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
267
|
// Decide whether to build this word via SHL/OR or as a single literal word
|
|
238
268
|
const literal = word.slice(seg[0].s);
|
|
239
269
|
const literalCost = 1 + literal.length;
|
|
240
|
-
|
|
270
|
+
let literalVal = 0n;
|
|
271
|
+
for (const b of literal) literalVal = (literalVal << 8n) | BigInt(b);
|
|
241
272
|
const baseBytes = Math.ceil(Math.log2(base + 1) / 8);
|
|
242
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
|
+
}
|
|
243
283
|
if (literalCost > 8) {
|
|
244
284
|
if (wordCache.has(wordHex)) {
|
|
245
285
|
if (literalCost > wordCacheCost.get(wordHex)! + baseBytes) {
|
|
@@ -257,19 +297,134 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
257
297
|
}
|
|
258
298
|
}
|
|
259
299
|
|
|
260
|
-
|
|
300
|
+
const byte8s = seg.every(({ s, e }) => s === e);
|
|
301
|
+
const byte8sCost = seg.length * 3; // each: PUSH1 (value), PUSH1 (offset), MSTORE8
|
|
302
|
+
if (inStack) {
|
|
261
303
|
emitPushB(literal);
|
|
262
304
|
} else {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
emitPushB(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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;
|
|
374
|
+
}
|
|
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
|
+
}
|
|
270
403
|
}
|
|
271
|
-
|
|
272
|
-
|
|
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!();
|
|
273
428
|
}
|
|
274
429
|
}
|
|
275
430
|
emitPushN(base);
|
|
@@ -281,20 +436,18 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
281
436
|
stack = [];
|
|
282
437
|
trackedMemSize = 0;
|
|
283
438
|
mem = new Map();
|
|
284
|
-
|
|
285
439
|
// Pre 2nd pass. Push most frequent literals into stack.
|
|
286
440
|
Array.from(stackFreq.entries())
|
|
287
|
-
.filter(([val, freq]) => freq > 1 && val !==
|
|
441
|
+
.filter(([val, freq]) => freq > 1 && val > 1n && val !== 32n && val !== 224n)
|
|
442
|
+
.sort((a, b) => stackCnt.get(b[0])! - stackCnt.get(a[0])!)
|
|
288
443
|
.filter(([val, _]) => {
|
|
289
|
-
return typeof val === 'number' ? val :
|
|
444
|
+
return typeof val === 'number' ? BigInt(val) : val <= MAX_128_BIT;
|
|
290
445
|
})
|
|
291
|
-
.
|
|
292
|
-
.slice(0, 14)
|
|
446
|
+
.slice(0, 15)
|
|
293
447
|
.forEach(([val, _]) => {
|
|
294
448
|
pushN(val);
|
|
295
449
|
});
|
|
296
|
-
|
|
297
|
-
stackFreq2 = new Map();
|
|
450
|
+
pushN(1n);
|
|
298
451
|
// Second pass: emit ops and track mem/stack
|
|
299
452
|
for (const step of plan) {
|
|
300
453
|
if (step.t === 'num') pushN(step.v);
|
|
@@ -312,7 +465,6 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
312
465
|
// - 0x5f35: PUSH0 CALLDATALOAD (address from calldata[0])
|
|
313
466
|
// - 0x5a: GAS (remaining gas)
|
|
314
467
|
// - 0xf1: CALL
|
|
315
|
-
// - 0x50: POP (discard success value)
|
|
316
468
|
//
|
|
317
469
|
// RETURNDATACOPY(destOffset=0, offset=0, length=RETURNDATASIZE):
|
|
318
470
|
// - 0x3d5f5f3e: RETURNDATASIZE PUSH0 PUSH0 RETURNDATACOPY
|
|
@@ -334,19 +486,21 @@ const _jitDecompressor = function (calldata: string): string {
|
|
|
334
486
|
// - CALLVALUE, load target address from calldata[0], GAS, CALL
|
|
335
487
|
// - RETURNDATACOPY(0, 0, RETURNDATASIZE)
|
|
336
488
|
// - RETURN(0, RETURNDATASIZE)
|
|
337
|
-
return '0x' + _uint8ArrayToHex(new Uint8Array(out)) + '
|
|
489
|
+
return '0x' + _uint8ArrayToHex(new Uint8Array(out)) + '345f355af13d5f5f3e3d5ff3';
|
|
338
490
|
};
|
|
339
491
|
|
|
340
|
-
const MIN_SIZE_FOR_COMPRESSION = 800;
|
|
341
492
|
const DECOMPRESSOR_ADDRESS = '0x00000000000000000000000000000000000000e0';
|
|
342
493
|
|
|
343
|
-
const _jit = 'jit';
|
|
344
|
-
const _flz = 'flz';
|
|
345
|
-
const _cd = 'cd';
|
|
346
|
-
|
|
347
494
|
/**
|
|
348
495
|
* Compresses eth_call payload using JIT, FastLZ (FLZ), or calldata RLE (CD) compression.
|
|
349
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
|
+
*
|
|
350
504
|
* @param payload - eth_call RPC payload
|
|
351
505
|
* @param alg - 'jit' | 'flz' | 'cd' | undefined (auto)
|
|
352
506
|
* @returns (un)compressed eth_call payload
|
|
@@ -354,45 +508,63 @@ const _cd = 'cd';
|
|
|
354
508
|
*/
|
|
355
509
|
//! @__PURE__
|
|
356
510
|
export const compress_call = function (payload: any, alg?: string): any {
|
|
357
|
-
const
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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;
|
|
526
|
+
}
|
|
366
527
|
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
const cd = alg === _cd || autoSelect ? LibZip.cdCompress(data) : null;
|
|
528
|
+
const originalSize = txObj.data.length;
|
|
529
|
+
if (originalSize < MIN_BODY_SIZE) return payload;
|
|
370
530
|
|
|
371
|
-
const
|
|
372
|
-
|
|
531
|
+
const inputData = '0x' + _normHex(txObj.data);
|
|
532
|
+
const to = txObj.to;
|
|
373
533
|
|
|
534
|
+
// Determine compression method and generate bytecode/calldata
|
|
374
535
|
let bytecode: string;
|
|
375
536
|
let calldata: string;
|
|
376
537
|
|
|
377
|
-
if (
|
|
378
|
-
bytecode = _jitDecompressor(
|
|
379
|
-
calldata = '0x' + _normHex(
|
|
538
|
+
if (alg === 'jit' || (!alg && (originalSize < 3000 || originalSize >= 8000))) {
|
|
539
|
+
bytecode = _jitDecompressor(inputData);
|
|
540
|
+
calldata = '0x' + _normHex(to).padStart(64, '0');
|
|
380
541
|
} else {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
+
}
|
|
384
557
|
}
|
|
385
558
|
|
|
386
|
-
|
|
387
|
-
if (
|
|
559
|
+
// Skip if not beneficial
|
|
560
|
+
if (bytecode.length + calldata.length >= originalSize) return payload;
|
|
388
561
|
|
|
389
562
|
return {
|
|
390
563
|
...payload,
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
...
|
|
395
|
-
|
|
396
|
-
},
|
|
564
|
+
params: [
|
|
565
|
+
{ ...txObj, to: DECOMPRESSOR_ADDRESS, data: calldata },
|
|
566
|
+
blockParam || 'latest',
|
|
567
|
+
{ ...overrides, [DECOMPRESSOR_ADDRESS]: { code: bytecode } },
|
|
568
|
+
],
|
|
397
569
|
};
|
|
398
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.",
|
|
@@ -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.23.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.7",
|
|
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.3",
|
|
119
|
+
"@types/node": "24.10.1",
|
|
120
|
+
"esbuild": "0.27.0",
|
|
121
|
+
"typescript": "5.9.3",
|
|
122
|
+
"viem": "2.40.2",
|
|
123
|
+
"vitest": "4.0.14"
|
|
123
124
|
},
|
|
124
125
|
"browserslist": [
|
|
125
126
|
">0.3%",
|