eth-compress 0.2.1 → 0.3.0
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 +72 -36
- package/_cjs/index.cjs +1 -2
- package/_cjs/index.cjs.map +4 -4
- package/_cjs/index.node.cjs +1 -2
- package/_cjs/index.node.cjs.map +4 -4
- package/_cjs/jit-compressor.cjs +2 -5
- package/_cjs/jit-compressor.cjs.map +4 -4
- package/_esm/index.js +1 -2
- package/_esm/index.js.map +4 -4
- package/_esm/index.node.js +1 -2
- package/_esm/index.node.js.map +4 -4
- package/_esm/jit-compressor.js +2 -5
- package/_esm/jit-compressor.js.map +4 -4
- package/_types/compiler/constants.d.ts +3 -0
- package/_types/compiler/constants.d.ts.map +1 -0
- package/_types/compiler/index.d.ts +4 -0
- package/_types/compiler/index.d.ts.map +1 -0
- package/_types/compiler/jit.d.ts +8 -0
- package/_types/compiler/jit.d.ts.map +1 -0
- package/_types/compiler/opcodes.d.ts +13 -0
- package/_types/compiler/opcodes.d.ts.map +1 -0
- package/_types/compiler/utils.d.ts +53 -0
- package/_types/compiler/utils.d.ts.map +1 -0
- package/_types/contracts.d.ts +15 -0
- package/_types/contracts.d.ts.map +1 -0
- package/_types/index.d.ts +10 -31
- package/_types/index.d.ts.map +1 -1
- package/_types/jit-compressor.d.ts +6 -20
- package/_types/jit-compressor.d.ts.map +1 -1
- package/index.node.ts +1 -1
- package/index.ts +64 -92
- package/jit-compressor.ts +81 -419
- package/package.json +9 -9
package/jit-compressor.ts
CHANGED
|
@@ -1,377 +1,17 @@
|
|
|
1
1
|
import { LibZip } from 'solady';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const _hexToUint8Array = (hex: string): Uint8Array => {
|
|
8
|
-
const normalized = _normHex(hex);
|
|
9
|
-
const len = normalized.length;
|
|
10
|
-
const bytes = new Uint8Array(len / 2);
|
|
11
|
-
for (let i = 0; i < len; i += 2) {
|
|
12
|
-
bytes[i / 2] = Number.parseInt(normalized.slice(i, i + 2), 16);
|
|
13
|
-
}
|
|
14
|
-
return bytes;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const _uint8ArrayToHex = (bytes: Uint8Array): string => {
|
|
18
|
-
let hex = '';
|
|
19
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
20
|
-
hex += bytes[i].toString(16).padStart(2, '0');
|
|
21
|
-
}
|
|
22
|
-
return hex;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Generates FastLZ (LZ77) decompressor bytecode. The generated code decompresses incoming calldata and forwards it to the target address.
|
|
27
|
-
* @param address - Target contract address
|
|
28
|
-
* @see {@link https://github.com/Vectorized/solady/blob/main/src/utils/LibZip.sol}
|
|
29
|
-
* @pure
|
|
30
|
-
*/
|
|
31
|
-
//! @__PURE__
|
|
32
|
-
export const flzFwdBytecode = (address: string): string =>
|
|
33
|
-
`0x365f73${_normHex(address)}815b838110602f575f80848134865af1503d5f803e3d5ff35b803590815f1a8060051c908115609857600190600783149285831a6007018118840218600201948383011a90601f1660081b0101808603906020811860208211021890815f5b80830151818a015201858110609257505050600201019201916018565b82906075565b6001929350829150019101925f5b82811060b3575001916018565b85851060c1575b60010160a6565b936001818192355f1a878501530194905060ba56`;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Generates RLE (run-length encoded) decompressor bytecode. The generated code decompresses incoming calldata and forwards it to the target address.
|
|
37
|
-
* @param address - Target contract address
|
|
38
|
-
* @see {@link https://github.com/Vectorized/solady/blob/main/src/utils/LibZip.sol}
|
|
39
|
-
* @pure
|
|
40
|
-
*/
|
|
41
|
-
//! @__PURE__
|
|
42
|
-
export const rleFwdBytecode = (address: string): string =>
|
|
43
|
-
`0x5f5f5b368110602d575f8083813473${_normHex(address)}5af1503d5f803e3d5ff35b600180820192909160031981019035185f1a8015604c57815301906002565b505f19815282820192607f9060031981019035185f1a818111156072575b160101906002565b838101368437606a56`;
|
|
44
|
-
|
|
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
|
-
const _jitDecompressor = function (calldata: string): string {
|
|
56
|
-
const hex = _normHex(calldata);
|
|
57
|
-
const originalBuf = _hexToUint8Array(hex);
|
|
58
|
-
|
|
59
|
-
// Right‑align the 4‑byte selector in the first 32‑byte slot (offset 28),
|
|
60
|
-
// so that everything after the selector is reconstructed on mostly
|
|
61
|
-
// word‑aligned boundaries. This keeps the ABI words (and therefore most
|
|
62
|
-
// calldata reconstruction) 32‑byte aligned in memory.
|
|
63
|
-
// That way we avoid encoding offsets for writes (most of the time),
|
|
64
|
-
const padding = 28;
|
|
65
|
-
const buf = new Uint8Array(padding + originalBuf.length);
|
|
66
|
-
buf.set(originalBuf, padding);
|
|
67
|
-
|
|
68
|
-
const n = buf.length;
|
|
69
|
-
|
|
70
|
-
let ops: number[] = [];
|
|
71
|
-
let data: (number[] | null)[] = [];
|
|
72
|
-
let stack: bigint[] = [];
|
|
73
|
-
let trackedMemSize = 0;
|
|
74
|
-
let mem = new Map<number, bigint>();
|
|
75
|
-
const getStackIdx = (val: bigint): number => {
|
|
76
|
-
const idx = stack.lastIndexOf(val);
|
|
77
|
-
return idx === -1 ? -1 : stack.length - 1 - idx;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const opFreq = new Map<number, number>();
|
|
81
|
-
const dataFreq = new Map<number[] | null, number>();
|
|
82
|
-
const stackFreq = new Map<bigint, number>();
|
|
83
|
-
const wordCache = new Map<string, number>();
|
|
84
|
-
const wordCacheCost = new Map<string, number>();
|
|
85
|
-
const roundUp32 = (x: number) => (x + 31) & ~31;
|
|
86
|
-
|
|
87
|
-
let pushCounter = 0;
|
|
88
|
-
const stackCnt = new Map<bigint, number>();
|
|
89
|
-
|
|
90
|
-
const pop2 = (): [bigint, bigint] => [stack.pop()!, stack.pop()!];
|
|
91
|
-
const MASK32 = (1n << 256n) - 1n;
|
|
92
|
-
|
|
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);
|
|
96
|
-
const pushOp = (op: number) => {
|
|
97
|
-
ops.push(op);
|
|
98
|
-
inc(opFreq, op);
|
|
99
|
-
};
|
|
100
|
-
const pushD = (d: number[] | null) => {
|
|
101
|
-
data.push(d || null);
|
|
102
|
-
inc(dataFreq, d || null);
|
|
103
|
-
};
|
|
104
|
-
const pushS = (v: bigint, freq: number = 1) => {
|
|
105
|
-
stack.push(v);
|
|
106
|
-
ctr(stackFreq, v, freq);
|
|
107
|
-
++pushCounter;
|
|
108
|
-
stackCnt.set(v, pushCounter);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const trackMem = (offset: number, size: number) => {
|
|
112
|
-
trackedMemSize = roundUp32(offset + size);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const addOp = (op: number, imm?: number[]) => {
|
|
116
|
-
if (op === 0x36) {
|
|
117
|
-
pushS(32n, 0);
|
|
118
|
-
} else if (op === 0x59) {
|
|
119
|
-
pushS(BigInt(trackedMemSize), 0);
|
|
120
|
-
} else if (op === 0x1b) {
|
|
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);
|
|
128
|
-
} else if (op === 0x17) {
|
|
129
|
-
// OR
|
|
130
|
-
const [a, b] = pop2();
|
|
131
|
-
if (ops[ops.length - 1] == 144) {
|
|
132
|
-
ops.pop();
|
|
133
|
-
data.pop();
|
|
134
|
-
}
|
|
135
|
-
pushS((a | b) & MASK32);
|
|
136
|
-
} else if ((op >= 0x60 && op <= 0x7f) || op === 0x5f) {
|
|
137
|
-
// PUSH
|
|
138
|
-
let v = 0n;
|
|
139
|
-
for (const b of imm || []) v = (v << 8n) | BigInt(b);
|
|
140
|
-
if (v == 224n) {
|
|
141
|
-
// Special‑case the literal 0xe0 (224):
|
|
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);
|
|
148
|
-
pushOp(0x30); // ADDRESS
|
|
149
|
-
pushD(null);
|
|
150
|
-
return;
|
|
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);
|
|
174
|
-
} else if (op === 0x51) {
|
|
175
|
-
// MLOAD
|
|
176
|
-
const k = Number(stack.pop()!);
|
|
177
|
-
pushS(mem.has(k) ? mem.get(k)! : 0n);
|
|
178
|
-
} else if (op === 0x52) {
|
|
179
|
-
// MSTORE
|
|
180
|
-
const [offset, value] = pop2();
|
|
181
|
-
const k = Number(offset);
|
|
182
|
-
mem.set(k, value & MASK32);
|
|
183
|
-
trackMem(k, 32);
|
|
184
|
-
} else if (op === 0x53) {
|
|
185
|
-
// MSTORE8
|
|
186
|
-
const [offset, _] = pop2();
|
|
187
|
-
trackMem(Number(offset), 1);
|
|
188
|
-
} else if (op === 0xf3) {
|
|
189
|
-
// RETURN
|
|
190
|
-
pop2();
|
|
191
|
-
}
|
|
192
|
-
pushOp(op);
|
|
193
|
-
pushD(imm || null);
|
|
194
|
-
};
|
|
195
|
-
const isInStack = (w) => stack.includes(w) || w == 0xe0 || w == 32n;
|
|
196
|
-
const op = (opcode: number) => addOp(opcode);
|
|
197
|
-
const pushN = (value: number | bigint) => {
|
|
198
|
-
if (value > 0 && value === trackedMemSize) return addOp(0x59);
|
|
199
|
-
if (value == 32n) return addOp(0x36);
|
|
200
|
-
if (!value) return addOp(0x5f, undefined); // PUSH0
|
|
201
|
-
let v = BigInt(value);
|
|
202
|
-
let bytes: number[] = [];
|
|
203
|
-
while (v) {
|
|
204
|
-
bytes.unshift(Number(v & 0xffn));
|
|
205
|
-
v >>= 8n;
|
|
206
|
-
}
|
|
207
|
-
return addOp(0x5f + bytes.length, bytes);
|
|
208
|
-
};
|
|
209
|
-
const pushB = (buf: Uint8Array) => addOp(0x5f + buf.length, Array.from(buf));
|
|
210
|
-
const cntWords = (hex: string, wordHex: string) =>
|
|
211
|
-
(hex.match(new RegExp(wordHex, 'g')) || []).length;
|
|
212
|
-
|
|
213
|
-
// Rough cost model
|
|
214
|
-
const estShlCost = (seg: Array<{ s: number; e: number }>) => {
|
|
215
|
-
let cost = 0;
|
|
216
|
-
let first = true;
|
|
217
|
-
for (const { s, e } of seg) {
|
|
218
|
-
cost += 1 + e - s + 1; // PUSH segLen bytes
|
|
219
|
-
if (31 - e > 0) cost += 1 /* PUSH1 */ + 1 /* shift byte */ + 1 /* SHL */;
|
|
220
|
-
if (!first) cost += 1; // OR
|
|
221
|
-
first = false;
|
|
222
|
-
}
|
|
223
|
-
return cost;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
type PlanStep =
|
|
227
|
-
| { t: 'num'; v: number | bigint }
|
|
228
|
-
| { t: 'bytes'; b: Uint8Array }
|
|
229
|
-
| { t: 'op'; o: number };
|
|
230
|
-
|
|
231
|
-
const plan: PlanStep[] = [];
|
|
232
|
-
const emitPushN = (v: number | bigint) => (plan.push({ t: 'num', v }), pushN(v));
|
|
233
|
-
const emitPushB = (b: Uint8Array) => (plan.push({ t: 'bytes', b }), pushB(b));
|
|
234
|
-
const emitOp = (o: number) => (plan.push({ t: 'op', o }), op(o));
|
|
235
|
-
pushN(1n);
|
|
236
|
-
// First pass: decide how to build each 32-byte word without emitting bytecode
|
|
237
|
-
for (let base = 0; base < n; base += 32) {
|
|
238
|
-
const word = new Uint8Array(32);
|
|
239
|
-
word.set(buf.slice(base, Math.min(base + 32, n)), 0);
|
|
240
|
-
|
|
241
|
-
const seg: Array<{ s: number; e: number }> = [];
|
|
242
|
-
for (let i = 0; i < 32; ) {
|
|
243
|
-
while (i < 32 && word[i] === 0) ++i;
|
|
244
|
-
if (i >= 32) break;
|
|
245
|
-
const s = i;
|
|
246
|
-
while (i < 32 && word[i] !== 0) ++i;
|
|
247
|
-
seg.push({ s, e: i - 1 });
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (!seg.length) continue;
|
|
251
|
-
|
|
252
|
-
// Decide whether to build this word via SHL/OR or as a single literal word
|
|
253
|
-
const literal = word.slice(seg[0].s);
|
|
254
|
-
const literalCost = 1 + literal.length;
|
|
255
|
-
let literalVal = 0n;
|
|
256
|
-
for (const b of literal) literalVal = (literalVal << 8n) | BigInt(b);
|
|
257
|
-
const baseBytes = Math.ceil(Math.log2(base + 1) / 8);
|
|
258
|
-
const wordHex = _uint8ArrayToHex(word);
|
|
259
|
-
if (literalCost > 8) {
|
|
260
|
-
if (wordCache.has(wordHex)) {
|
|
261
|
-
if (literalCost > wordCacheCost.get(wordHex)! + baseBytes) {
|
|
262
|
-
emitPushN(wordCache.get(wordHex)!);
|
|
263
|
-
emitOp(0x51);
|
|
264
|
-
emitPushN(base);
|
|
265
|
-
emitOp(0x52); // MSTORE
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
} else if (wordCacheCost.get(wordHex) != -1) {
|
|
269
|
-
const reuseCost = baseBytes + 3;
|
|
270
|
-
const freq = cntWords(hex, wordHex);
|
|
271
|
-
wordCacheCost.set(wordHex, freq * 32 > freq * reuseCost ? reuseCost : -1);
|
|
272
|
-
wordCache.set(wordHex, base);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
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)) {
|
|
289
|
-
emitPushB(literal);
|
|
290
|
-
} else {
|
|
291
|
-
let first = true;
|
|
292
|
-
for (const { s, e } of seg) {
|
|
293
|
-
const suffix0s = 31 - e;
|
|
294
|
-
emitPushB(word.slice(s, e + 1));
|
|
295
|
-
if (suffix0s > 0) {
|
|
296
|
-
emitPushN(suffix0s * 8);
|
|
297
|
-
emitOp(0x1b); // SHL
|
|
298
|
-
}
|
|
299
|
-
if (!first) emitOp(0x17); // OR
|
|
300
|
-
first = false;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
emitPushN(base);
|
|
304
|
-
emitOp(0x52); // MSTORE
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
ops = [];
|
|
308
|
-
data = [];
|
|
309
|
-
stack = [];
|
|
310
|
-
trackedMemSize = 0;
|
|
311
|
-
mem = new Map();
|
|
312
|
-
// Pre 2nd pass. Push most frequent literals into stack.
|
|
313
|
-
Array.from(stackFreq.entries())
|
|
314
|
-
.filter(([val, freq]) => freq > 1 && val > 1n && val !== 32n && val !== 224n)
|
|
315
|
-
.sort((a, b) => stackCnt.get(b[0])! - stackCnt.get(a[0])!)
|
|
316
|
-
.filter(([val, _]) => {
|
|
317
|
-
return typeof val === 'number' ? BigInt(val) : val <= MAX_160_BIT;
|
|
318
|
-
})
|
|
319
|
-
.slice(0, 13)
|
|
320
|
-
.forEach(([val, _]) => {
|
|
321
|
-
pushN(val);
|
|
322
|
-
});
|
|
323
|
-
pushN(1n);
|
|
324
|
-
// Second pass: emit ops and track mem/stack
|
|
325
|
-
for (const step of plan) {
|
|
326
|
-
if (step.t === 'num') pushN(step.v);
|
|
327
|
-
else if (step.t === 'bytes') pushB(step.b);
|
|
328
|
-
else if (step.t === 'op') op(step.o);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// CALL stack layout (top to bottom): gas, address, value, argsOffset, argsSize, retOffset, retSize
|
|
332
|
-
//
|
|
333
|
-
// Opcodes breakdown:
|
|
334
|
-
// - 0x5f5f: PUSH0 PUSH0 (retSize=0, retOffset=0)
|
|
335
|
-
// - pushN(originalBuf.length): argsSize = actual data length
|
|
336
|
-
// - pushN(padding): argsOffset (skip leading alignment bytes)
|
|
337
|
-
// - 0x34: CALLVALUE (value)
|
|
338
|
-
// - 0x5f35: PUSH0 CALLDATALOAD (address from calldata[0])
|
|
339
|
-
// - 0x5a: GAS (remaining gas)
|
|
340
|
-
// - 0xf1: CALL
|
|
341
|
-
//
|
|
342
|
-
// RETURNDATACOPY(destOffset=0, offset=0, length=RETURNDATASIZE):
|
|
343
|
-
// - 0x3d5f5f3e: RETURNDATASIZE PUSH0 PUSH0 RETURNDATACOPY
|
|
344
|
-
//
|
|
345
|
-
// RETURN(offset=0, size=RETURNDATASIZE):
|
|
346
|
-
// - 0x3d5ff3: RETURNDATASIZE PUSH0 RETURN
|
|
347
|
-
|
|
348
|
-
op(0x5f); // PUSH0 (retSize)
|
|
349
|
-
op(0x5f); // PUSH0 (retOffset)
|
|
350
|
-
pushN(originalBuf.length); // argsSize = actual data length
|
|
351
|
-
pushN(padding); // argsOffset = padding
|
|
352
|
-
|
|
353
|
-
const out: number[] = [];
|
|
354
|
-
for (let i = 0; i < ops.length; ++i) {
|
|
355
|
-
out.push(ops[i]);
|
|
356
|
-
if (ops[i] >= 0x60 && ops[i] <= 0x7f && data[i]) out.push(...data[i]!);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// - CALLVALUE, load target address from calldata[0], GAS, CALL
|
|
360
|
-
// - RETURNDATACOPY(0, 0, RETURNDATASIZE)
|
|
361
|
-
// - RETURN(0, RETURNDATASIZE)
|
|
362
|
-
return '0x' + _uint8ArrayToHex(new Uint8Array(out)) + '345f355af13d5f5f3e3d5ff3';
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
const MIN_SIZE_FOR_COMPRESSION = 1150;
|
|
366
|
-
const DECOMPRESSOR_ADDRESS = '0x00000000000000000000000000000000000000e0';
|
|
367
|
-
|
|
368
|
-
const _jit = 'jit';
|
|
369
|
-
const _flz = 'flz';
|
|
370
|
-
const _cd = 'cd';
|
|
371
|
-
|
|
2
|
+
import { _jitDecompressor } from './compiler';
|
|
3
|
+
import { _normHex } from './compiler/utils';
|
|
4
|
+
import { flzFwdBytecode, rleFwdBytecode } from './contracts';
|
|
5
|
+
import { MIN_BODY_SIZE } from './index';
|
|
372
6
|
/**
|
|
373
7
|
* Compresses eth_call payload using JIT, FastLZ (FLZ), or calldata RLE (CD) compression.
|
|
374
8
|
* Auto-selects best algorithm if not specified. Only compresses if >800 bytes and beneficial.
|
|
9
|
+
*
|
|
10
|
+
* Only applies compression to calls that:
|
|
11
|
+
* - have no state overrides
|
|
12
|
+
* - have a target address and calldata
|
|
13
|
+
* - have no other properties (nonce, gas, etc.)
|
|
14
|
+
*
|
|
375
15
|
* @param payload - eth_call RPC payload
|
|
376
16
|
* @param alg - 'jit' | 'flz' | 'cd' | undefined (auto)
|
|
377
17
|
* @returns (un)compressed eth_call payload
|
|
@@ -379,65 +19,87 @@ const _cd = 'cd';
|
|
|
379
19
|
*/
|
|
380
20
|
//! @__PURE__
|
|
381
21
|
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
|
-
|
|
22
|
+
const { method, params } = payload;
|
|
23
|
+
if (method && method !== 'eth_call') return payload;
|
|
24
|
+
const txObj = params?.[0] || payload;
|
|
25
|
+
const blockParam = params?.[1];
|
|
26
|
+
const overrides = params?.[2];
|
|
27
|
+
|
|
28
|
+
// Validation
|
|
29
|
+
if (
|
|
30
|
+
(overrides && Object.keys(overrides).length > 0) ||
|
|
31
|
+
!txObj?.to ||
|
|
32
|
+
!txObj?.data ||
|
|
33
|
+
Object.keys(txObj).some((k) => !['to', 'data', 'from'].includes(k))
|
|
34
|
+
) {
|
|
35
|
+
return payload;
|
|
36
|
+
}
|
|
394
37
|
|
|
395
|
-
const
|
|
396
|
-
|
|
38
|
+
const originalSize = txObj.data.length;
|
|
39
|
+
if (originalSize < MIN_BODY_SIZE) return payload;
|
|
397
40
|
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
const
|
|
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
|
-
}
|
|
41
|
+
const inputData = '0x' + _normHex(txObj.data);
|
|
42
|
+
const to = txObj.to;
|
|
43
|
+
const from = txObj.from;
|
|
412
44
|
|
|
413
45
|
let bytecode: string;
|
|
414
46
|
let calldata: string;
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
47
|
+
let decompressorAddress: string;
|
|
48
|
+
let fromAddr: string | undefined;
|
|
49
|
+
let balanceHex: string;
|
|
50
|
+
|
|
51
|
+
if (alg === 'jit' || (!alg && (originalSize < 3000 || originalSize >= 8000))) {
|
|
52
|
+
const result = _jitDecompressor(inputData, to, from);
|
|
53
|
+
bytecode = result.bytecode;
|
|
54
|
+
calldata = result.calldata;
|
|
55
|
+
decompressorAddress = result.to;
|
|
56
|
+
fromAddr = result.from;
|
|
57
|
+
balanceHex = result.balance;
|
|
419
58
|
} else {
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
59
|
+
const jit = !alg ? _jitDecompressor(inputData, to, from) : null;
|
|
60
|
+
const flzData = alg === 'flz' || !alg ? LibZip.flzCompress(inputData) : null;
|
|
61
|
+
const cdData = alg === 'cd' || (!alg && flzData) ? LibZip.cdCompress(inputData) : null;
|
|
62
|
+
const useFlz =
|
|
63
|
+
alg === 'flz' || (!alg && flzData && (!cdData || flzData.length < cdData.length));
|
|
64
|
+
|
|
65
|
+
if (useFlz) {
|
|
66
|
+
calldata = flzData!;
|
|
67
|
+
bytecode = flzFwdBytecode(to);
|
|
68
|
+
} else {
|
|
69
|
+
calldata = cdData!;
|
|
70
|
+
bytecode = rleFwdBytecode(to);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
decompressorAddress = '0x' + 224n.toString(16).padStart(40, '0');
|
|
74
|
+
fromAddr = from ? _normHex(from).padStart(16, '0') : undefined;
|
|
75
|
+
balanceHex = '0';
|
|
76
|
+
if (
|
|
77
|
+
!alg &&
|
|
78
|
+
jit &&
|
|
79
|
+
jit.bytecode.length + jit.calldata.length < bytecode.length + calldata.length
|
|
80
|
+
) {
|
|
81
|
+
bytecode = jit.bytecode;
|
|
82
|
+
calldata = jit.calldata;
|
|
83
|
+
decompressorAddress = jit.to;
|
|
84
|
+
fromAddr = jit.from;
|
|
85
|
+
balanceHex = jit.balance;
|
|
86
|
+
}
|
|
423
87
|
}
|
|
424
88
|
|
|
425
|
-
|
|
426
|
-
if (
|
|
89
|
+
// Skip if not beneficial
|
|
90
|
+
if (bytecode.length + calldata.length >= originalSize) return payload;
|
|
91
|
+
|
|
92
|
+
const stateOverride: any = {
|
|
93
|
+
code: bytecode,
|
|
94
|
+
balance: '0x' + balanceHex,
|
|
95
|
+
};
|
|
96
|
+
const compressedTxObj: any = { to: decompressorAddress, data: calldata };
|
|
97
|
+
if (fromAddr) {
|
|
98
|
+
compressedTxObj.from = '0x' + fromAddr;
|
|
99
|
+
}
|
|
427
100
|
|
|
428
101
|
return {
|
|
429
102
|
...payload,
|
|
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
|
-
],
|
|
103
|
+
params: [compressedTxObj, blockParam, { ...overrides, [decompressorAddress]: stateOverride }],
|
|
442
104
|
};
|
|
443
105
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eth-compress",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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.",
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"latency",
|
|
13
13
|
"viem"
|
|
14
14
|
],
|
|
15
|
-
"bugs": "https://github.com/tadpole-labs/eth-compress/issues",
|
|
16
15
|
"repository": {
|
|
17
16
|
"type": "git",
|
|
18
17
|
"url": "git+https://github.com/tadpole-labs/eth-compress.git"
|
|
19
18
|
},
|
|
20
19
|
"homepage": "https://github.com/tadpole-labs/eth-compress",
|
|
20
|
+
"bugs": "https://github.com/tadpole-labs/eth-compress/issues",
|
|
21
21
|
"license": "Apache-2.0 OR MIT",
|
|
22
22
|
"main": "./_esm/index.node.js",
|
|
23
23
|
"types": "./_types/index.d.ts",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"node": ">=22",
|
|
100
100
|
"pnpm": ">=10"
|
|
101
101
|
},
|
|
102
|
-
"packageManager": "pnpm@10.
|
|
102
|
+
"packageManager": "pnpm@10.25.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.8",
|
|
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.
|
|
119
|
-
"@types/node": "
|
|
120
|
-
"esbuild": "0.27.
|
|
118
|
+
"@types/bun": "1.3.4",
|
|
119
|
+
"@types/node": "25.0.1",
|
|
120
|
+
"esbuild": "0.27.1",
|
|
121
121
|
"typescript": "5.9.3",
|
|
122
|
-
"viem": "2.
|
|
123
|
-
"vitest": "4.0.
|
|
122
|
+
"viem": "2.41.2",
|
|
123
|
+
"vitest": "4.0.15"
|
|
124
124
|
},
|
|
125
125
|
"browserslist": [
|
|
126
126
|
">0.3%",
|