eth-compress 0.2.2 → 0.3.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 +65 -56
- 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 -4
- 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 -4
- 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 +9 -0
- package/_types/compiler/jit.d.ts.map +1 -0
- package/_types/compiler/opcodes.d.ts +12 -0
- package/_types/compiler/opcodes.d.ts.map +1 -0
- package/_types/compiler/utils.d.ts +42 -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 +9 -31
- package/_types/index.d.ts.map +1 -1
- package/_types/jit-compressor.d.ts +0 -1
- package/_types/jit-compressor.d.ts.map +1 -1
- package/index.ts +61 -93
- package/jit-compressor.ts +45 -507
- package/package.json +9 -9
package/jit-compressor.ts
CHANGED
|
@@ -1,502 +1,13 @@
|
|
|
1
1
|
import { LibZip } from 'solady';
|
|
2
|
+
import { _jitDecompressor, DEC_ADDR } from './compiler';
|
|
3
|
+
import { _normHex } from './compiler/utils';
|
|
4
|
+
import { flzFwdBytecode, rleFwdBytecode } from './contracts';
|
|
2
5
|
import { MIN_BODY_SIZE } from './index';
|
|
3
|
-
|
|
4
|
-
const MAX_128_BIT = (1n << 128n) - 1n;
|
|
5
|
-
const MAX_256_BIT = (1n << 256n) - 1n;
|
|
6
|
-
|
|
7
|
-
const _normHex = (hex: string): string => hex.replace(/^0x/, '').toLowerCase();
|
|
8
|
-
|
|
9
|
-
const _hexToUint8Array = (hex: string): Uint8Array => {
|
|
10
|
-
const normalized = _normHex(hex);
|
|
11
|
-
const len = normalized.length;
|
|
12
|
-
const bytes = new Uint8Array(len / 2);
|
|
13
|
-
for (let i = 0; i < len; i += 2) {
|
|
14
|
-
bytes[i / 2] = Number.parseInt(normalized.slice(i, i + 2), 16);
|
|
15
|
-
}
|
|
16
|
-
return bytes;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const _uint8ArrayToHex = (bytes: Uint8Array): string => {
|
|
20
|
-
let hex = '';
|
|
21
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
22
|
-
hex += bytes[i].toString(16).padStart(2, '0');
|
|
23
|
-
}
|
|
24
|
-
return hex;
|
|
25
|
-
};
|
|
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
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Generates FastLZ (LZ77) decompressor bytecode. The generated code decompresses incoming calldata and forwards it to the target address.
|
|
45
|
-
* @param address - Target contract address
|
|
46
|
-
* @see {@link https://github.com/Vectorized/solady/blob/main/src/utils/LibZip.sol}
|
|
47
|
-
* @pure
|
|
48
|
-
*/
|
|
49
|
-
//! @__PURE__
|
|
50
|
-
const flzFwdBytecode = (address: string): string =>
|
|
51
|
-
`0x365f73${_normHex(address)}815b838110602f575f80848134865af1503d5f803e3d5ff35b803590815f1a8060051c908115609857600190600783149285831a6007018118840218600201948383011a90601f1660081b0101808603906020811860208211021890815f5b80830151818a015201858110609257505050600201019201916018565b82906075565b6001929350829150019101925f5b82811060b3575001916018565b85851060c1575b60010160a6565b936001818192355f1a878501530194905060ba56`;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Generates RLE (run-length encoded) decompressor bytecode. The generated code decompresses incoming calldata and forwards it to the target address.
|
|
55
|
-
* @param address - Target contract address
|
|
56
|
-
* @see {@link https://github.com/Vectorized/solady/blob/main/src/utils/LibZip.sol}
|
|
57
|
-
* @pure
|
|
58
|
-
*/
|
|
59
|
-
//! @__PURE__
|
|
60
|
-
const rleFwdBytecode = (address: string): string =>
|
|
61
|
-
`0x5f5f5b368110602d575f8083813473${_normHex(address)}5af1503d5f803e3d5ff35b600180820192909160031981019035185f1a8015604c57815301906002565b505f19815282820192607f9060031981019035185f1a818111156072575b160101906002565b838101368437606a56`;
|
|
62
|
-
|
|
63
|
-
const _jitDecompressor = function (calldata: string): string {
|
|
64
|
-
const hex = _normHex(calldata);
|
|
65
|
-
const originalBuf = _hexToUint8Array(hex);
|
|
66
|
-
|
|
67
|
-
// Right‑align the 4‑byte selector in the first 32‑byte slot (offset 28),
|
|
68
|
-
// so that everything after the selector is reconstructed on mostly
|
|
69
|
-
// word‑aligned boundaries. This keeps the ABI words (and therefore most
|
|
70
|
-
// calldata reconstruction) 32‑byte aligned in memory.
|
|
71
|
-
// That way we avoid encoding offsets for writes (most of the time),
|
|
72
|
-
const padding = 28;
|
|
73
|
-
const buf = new Uint8Array(padding + originalBuf.length);
|
|
74
|
-
buf.set(originalBuf, padding);
|
|
75
|
-
|
|
76
|
-
const n = buf.length;
|
|
77
|
-
|
|
78
|
-
let ops: number[] = [];
|
|
79
|
-
let data: (number[] | null)[] = [];
|
|
80
|
-
let stack: bigint[] = [];
|
|
81
|
-
let trackedMemSize = 0;
|
|
82
|
-
let mem = new Map<number, bigint>();
|
|
83
|
-
let firstPass = true;
|
|
84
|
-
const getStackIdx = (val: bigint): number => {
|
|
85
|
-
let idx = stack.lastIndexOf(val);
|
|
86
|
-
idx = idx === -1 ? -1 : stack.length - 1 - idx;
|
|
87
|
-
return idx > 15 ? -1 : idx;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const opFreq = new Map<number, number>();
|
|
91
|
-
const dataFreq = new Map<number[] | null, number>();
|
|
92
|
-
const stackFreq = new Map<bigint, number>();
|
|
93
|
-
const wordCache = new Map<string, number>();
|
|
94
|
-
const wordCacheCost = new Map<string, number>();
|
|
95
|
-
const roundUp32 = (x: number) => (x + 31) & ~31;
|
|
96
|
-
|
|
97
|
-
let pushCounter = 0;
|
|
98
|
-
const stackCnt = new Map<bigint, number>();
|
|
99
|
-
const pop2 = (): [bigint, bigint] => [stack.pop()!, stack.pop()!];
|
|
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) => {
|
|
103
|
-
ops.push(op);
|
|
104
|
-
inc(opFreq, op);
|
|
105
|
-
const imm = d ?? null;
|
|
106
|
-
data.push(imm);
|
|
107
|
-
inc(dataFreq, imm);
|
|
108
|
-
};
|
|
109
|
-
const pushS = (v: bigint, freq: number = 1) => {
|
|
110
|
-
stack.push(v);
|
|
111
|
-
ctr(stackFreq, v, freq);
|
|
112
|
-
++pushCounter;
|
|
113
|
-
stackCnt.set(v, pushCounter);
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const trackMem = (offset: number, size: number) => {
|
|
117
|
-
trackedMemSize = roundUp32(offset + size);
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const addOp = (op: number, imm?: number[]) => {
|
|
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);
|
|
145
|
-
} else if (op === 0x1b) {
|
|
146
|
-
// SHL
|
|
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);
|
|
153
|
-
} else if (op === 0x17) {
|
|
154
|
-
// OR
|
|
155
|
-
let [a, b] = pop2();
|
|
156
|
-
pushS(or(a, b), 1);
|
|
157
|
-
} else if ((op >= 0x60 && op <= 0x7f) || op === 0x5f) {
|
|
158
|
-
// PUSH
|
|
159
|
-
let v = 0n;
|
|
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
|
-
}
|
|
176
|
-
const idx = getStackIdx(v);
|
|
177
|
-
if (idx !== -1 && op != 0x5f) {
|
|
178
|
-
let pushCtr = firstPass ? 1 : -1;
|
|
179
|
-
pushS(v, pushCtr);
|
|
180
|
-
pushOp(128 + idx);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (v == MAX_256_BIT) {
|
|
184
|
-
pushS(v);
|
|
185
|
-
pushOp(0x5f); // 0
|
|
186
|
-
pushOp(0x19); // NOT
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
pushS(v);
|
|
190
|
-
} else if (op === 0x51) {
|
|
191
|
-
// MLOAD
|
|
192
|
-
const k = Number(stack.pop()!);
|
|
193
|
-
pushS(mem.has(k) ? mem.get(k)! : 0n);
|
|
194
|
-
} else if (op === 0x52) {
|
|
195
|
-
// MSTORE
|
|
196
|
-
const [offset, value] = pop2();
|
|
197
|
-
const k = Number(offset);
|
|
198
|
-
mem.set(k, value & MAX_256_BIT);
|
|
199
|
-
trackMem(k, 32);
|
|
200
|
-
} else if (op === 0x53) {
|
|
201
|
-
// MSTORE8
|
|
202
|
-
const [offset, _] = pop2();
|
|
203
|
-
trackMem(Number(offset), 1);
|
|
204
|
-
} else if (op === 0xf3) {
|
|
205
|
-
// RETURN
|
|
206
|
-
pop2();
|
|
207
|
-
}
|
|
208
|
-
pushOp(op, imm || null);
|
|
209
|
-
};
|
|
210
|
-
const op = (opcode: number) => addOp(opcode);
|
|
211
|
-
const pushN = (value: number | bigint) => {
|
|
212
|
-
if (value > 0 && value === trackedMemSize) return addOp(0x59);
|
|
213
|
-
if (value == 32n) return addOp(0x36);
|
|
214
|
-
if (!value) return addOp(0x5f, undefined); // PUSH0
|
|
215
|
-
let v = BigInt(value);
|
|
216
|
-
let bytes: number[] = [];
|
|
217
|
-
while (v) {
|
|
218
|
-
bytes.unshift(Number(v & 0xffn));
|
|
219
|
-
v >>= 8n;
|
|
220
|
-
}
|
|
221
|
-
return addOp(0x5f + bytes.length, bytes);
|
|
222
|
-
};
|
|
223
|
-
const pushB = (buf: Uint8Array) => addOp(0x5f + buf.length, Array.from(buf));
|
|
224
|
-
const cntWords = (hex: string, wordHex: string) =>
|
|
225
|
-
(hex.match(new RegExp(wordHex, 'g')) || []).length;
|
|
226
|
-
|
|
227
|
-
// Rough cost model
|
|
228
|
-
const estShlCost = (seg: Array<{ s: number; e: number }>) => {
|
|
229
|
-
let cost = 0;
|
|
230
|
-
let first = true;
|
|
231
|
-
for (const { s, e } of seg) {
|
|
232
|
-
cost += 1 + e - s + 1; // PUSH segLen bytes
|
|
233
|
-
if (31 - e > 0) cost += 1 /* PUSH1 */ + 1 /* shift byte */ + 1 /* SHL */;
|
|
234
|
-
if (!first) cost += 1; // OR
|
|
235
|
-
first = false;
|
|
236
|
-
}
|
|
237
|
-
return cost;
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
type PlanStep =
|
|
241
|
-
| { t: 'num'; v: number | bigint }
|
|
242
|
-
| { t: 'bytes'; b: Uint8Array }
|
|
243
|
-
| { t: 'op'; o: number };
|
|
244
|
-
|
|
245
|
-
const plan: PlanStep[] = [];
|
|
246
|
-
const emitPushN = (v: number | bigint) => (plan.push({ t: 'num', v }), pushN(v));
|
|
247
|
-
const emitPushB = (b: Uint8Array) => (plan.push({ t: 'bytes', b }), pushB(b));
|
|
248
|
-
const emitOp = (o: number) => (plan.push({ t: 'op', o }), op(o));
|
|
249
|
-
pushN(1n);
|
|
250
|
-
// First pass: decide how to build each 32-byte word without emitting bytecode
|
|
251
|
-
const _stack = [1n, 32n, 224n];
|
|
252
|
-
for (let base = 0; base < n; base += 32) {
|
|
253
|
-
const word = new Uint8Array(32);
|
|
254
|
-
word.set(buf.slice(base, Math.min(base + 32, n)), 0);
|
|
255
|
-
|
|
256
|
-
const seg: Array<{ s: number; e: number }> = [];
|
|
257
|
-
for (let i = 0; i < 32; ) {
|
|
258
|
-
while (i < 32 && word[i] === 0) ++i;
|
|
259
|
-
if (i >= 32) break;
|
|
260
|
-
const s = i;
|
|
261
|
-
while (i < 32 && word[i] !== 0) ++i;
|
|
262
|
-
seg.push({ s, e: i - 1 });
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (!seg.length) continue;
|
|
266
|
-
|
|
267
|
-
// Decide whether to build this word via SHL/OR or as a single literal word
|
|
268
|
-
const literal = word.slice(seg[0].s);
|
|
269
|
-
const literalCost = 1 + literal.length;
|
|
270
|
-
let literalVal = 0n;
|
|
271
|
-
for (const b of literal) literalVal = (literalVal << 8n) | BigInt(b);
|
|
272
|
-
const baseBytes = Math.ceil(Math.log2(base + 1) / 8);
|
|
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
|
-
}
|
|
283
|
-
if (literalCost > 8) {
|
|
284
|
-
if (wordCache.has(wordHex)) {
|
|
285
|
-
if (literalCost > wordCacheCost.get(wordHex)! + baseBytes) {
|
|
286
|
-
emitPushN(wordCache.get(wordHex)!);
|
|
287
|
-
emitOp(0x51);
|
|
288
|
-
emitPushN(base);
|
|
289
|
-
emitOp(0x52); // MSTORE
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
} else if (wordCacheCost.get(wordHex) != -1) {
|
|
293
|
-
const reuseCost = baseBytes + 3;
|
|
294
|
-
const freq = cntWords(hex, wordHex);
|
|
295
|
-
wordCacheCost.set(wordHex, freq * 32 > freq * reuseCost ? reuseCost : -1);
|
|
296
|
-
wordCache.set(wordHex, base);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const byte8s = seg.every(({ s, e }) => s === e);
|
|
301
|
-
const byte8sCost = seg.length * 3; // each: PUSH1 (value), PUSH1 (offset), MSTORE8
|
|
302
|
-
if (inStack) {
|
|
303
|
-
emitPushB(literal);
|
|
304
|
-
} else {
|
|
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
|
-
}
|
|
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!();
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
emitPushN(base);
|
|
431
|
-
emitOp(0x52); // MSTORE
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
ops = [];
|
|
435
|
-
data = [];
|
|
436
|
-
stack = [];
|
|
437
|
-
trackedMemSize = 0;
|
|
438
|
-
mem = new Map();
|
|
439
|
-
// Pre 2nd pass. Push most frequent literals into stack.
|
|
440
|
-
Array.from(stackFreq.entries())
|
|
441
|
-
.filter(([val, freq]) => freq > 1 && val > 1n && val !== 32n && val !== 224n)
|
|
442
|
-
.sort((a, b) => stackCnt.get(b[0])! - stackCnt.get(a[0])!)
|
|
443
|
-
.filter(([val, _]) => {
|
|
444
|
-
return typeof val === 'number' ? BigInt(val) : val <= MAX_128_BIT;
|
|
445
|
-
})
|
|
446
|
-
.slice(0, 15)
|
|
447
|
-
.forEach(([val, _]) => {
|
|
448
|
-
pushN(val);
|
|
449
|
-
});
|
|
450
|
-
pushN(1n);
|
|
451
|
-
// Second pass: emit ops and track mem/stack
|
|
452
|
-
for (const step of plan) {
|
|
453
|
-
if (step.t === 'num') pushN(step.v);
|
|
454
|
-
else if (step.t === 'bytes') pushB(step.b);
|
|
455
|
-
else if (step.t === 'op') op(step.o);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// CALL stack layout (top to bottom): gas, address, value, argsOffset, argsSize, retOffset, retSize
|
|
459
|
-
//
|
|
460
|
-
// Opcodes breakdown:
|
|
461
|
-
// - 0x5f5f: PUSH0 PUSH0 (retSize=0, retOffset=0)
|
|
462
|
-
// - pushN(originalBuf.length): argsSize = actual data length
|
|
463
|
-
// - pushN(padding): argsOffset (skip leading alignment bytes)
|
|
464
|
-
// - 0x34: CALLVALUE (value)
|
|
465
|
-
// - 0x5f35: PUSH0 CALLDATALOAD (address from calldata[0])
|
|
466
|
-
// - 0x5a: GAS (remaining gas)
|
|
467
|
-
// - 0xf1: CALL
|
|
468
|
-
//
|
|
469
|
-
// RETURNDATACOPY(destOffset=0, offset=0, length=RETURNDATASIZE):
|
|
470
|
-
// - 0x3d5f5f3e: RETURNDATASIZE PUSH0 PUSH0 RETURNDATACOPY
|
|
471
|
-
//
|
|
472
|
-
// RETURN(offset=0, size=RETURNDATASIZE):
|
|
473
|
-
// - 0x3d5ff3: RETURNDATASIZE PUSH0 RETURN
|
|
474
|
-
|
|
475
|
-
op(0x5f); // PUSH0 (retSize)
|
|
476
|
-
op(0x5f); // PUSH0 (retOffset)
|
|
477
|
-
pushN(originalBuf.length); // argsSize = actual data length
|
|
478
|
-
pushN(padding); // argsOffset = padding
|
|
479
|
-
|
|
480
|
-
const out: number[] = [];
|
|
481
|
-
for (let i = 0; i < ops.length; ++i) {
|
|
482
|
-
out.push(ops[i]);
|
|
483
|
-
if (ops[i] >= 0x60 && ops[i] <= 0x7f && data[i]) out.push(...data[i]!);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// - CALLVALUE, load target address from calldata[0], GAS, CALL
|
|
487
|
-
// - RETURNDATACOPY(0, 0, RETURNDATASIZE)
|
|
488
|
-
// - RETURN(0, RETURNDATASIZE)
|
|
489
|
-
return '0x' + _uint8ArrayToHex(new Uint8Array(out)) + '345f355af13d5f5f3e3d5ff3';
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
const DECOMPRESSOR_ADDRESS = '0x00000000000000000000000000000000000000e0';
|
|
493
|
-
|
|
494
6
|
/**
|
|
495
7
|
* Compresses eth_call payload using JIT, FastLZ (FLZ), or calldata RLE (CD) compression.
|
|
496
8
|
* Auto-selects best algorithm if not specified. Only compresses if >800 bytes and beneficial.
|
|
497
9
|
*
|
|
498
10
|
* Only applies compression to calls that:
|
|
499
|
-
* - target the latest block ID
|
|
500
11
|
* - have no state overrides
|
|
501
12
|
* - have a target address and calldata
|
|
502
13
|
* - have no other properties (nonce, gas, etc.)
|
|
@@ -516,11 +27,13 @@ export const compress_call = function (payload: any, alg?: string): any {
|
|
|
516
27
|
|
|
517
28
|
// Validation
|
|
518
29
|
if (
|
|
519
|
-
(blockParam && blockParam !== 'latest') ||
|
|
520
|
-
(overrides && Object.keys(overrides).length > 0) ||
|
|
521
30
|
!txObj?.to ||
|
|
522
31
|
!txObj?.data ||
|
|
523
|
-
|
|
32
|
+
(() => {
|
|
33
|
+
if (overrides) for (const _ in overrides) return true;
|
|
34
|
+
for (const k in txObj) if (k !== 'to' && k !== 'data' && k !== 'from') return true;
|
|
35
|
+
return false;
|
|
36
|
+
})()
|
|
524
37
|
) {
|
|
525
38
|
return payload;
|
|
526
39
|
}
|
|
@@ -528,22 +41,27 @@ export const compress_call = function (payload: any, alg?: string): any {
|
|
|
528
41
|
const originalSize = txObj.data.length;
|
|
529
42
|
if (originalSize < MIN_BODY_SIZE) return payload;
|
|
530
43
|
|
|
531
|
-
const inputData =
|
|
44
|
+
const inputData = txObj.data;
|
|
532
45
|
const to = txObj.to;
|
|
46
|
+
const from = txObj.from;
|
|
533
47
|
|
|
534
|
-
// Determine compression method and generate bytecode/calldata
|
|
535
48
|
let bytecode: string;
|
|
536
49
|
let calldata: string;
|
|
50
|
+
let decompressorAddress: string;
|
|
51
|
+
let fromAddr: string | undefined;
|
|
52
|
+
let balanceHex: string;
|
|
537
53
|
|
|
538
54
|
if (alg === 'jit' || (!alg && (originalSize < 3000 || originalSize >= 8000))) {
|
|
539
|
-
|
|
540
|
-
|
|
55
|
+
const result = _jitDecompressor(inputData, to, from);
|
|
56
|
+
bytecode = result.bytecode;
|
|
57
|
+
calldata = result.calldata;
|
|
58
|
+
decompressorAddress = result.to;
|
|
59
|
+
fromAddr = result.from;
|
|
60
|
+
balanceHex = result.balance;
|
|
541
61
|
} else {
|
|
542
|
-
|
|
62
|
+
const jit = !alg ? _jitDecompressor(inputData, to, from) : null;
|
|
543
63
|
const flzData = alg === 'flz' || !alg ? LibZip.flzCompress(inputData) : null;
|
|
544
64
|
const cdData = alg === 'cd' || (!alg && flzData) ? LibZip.cdCompress(inputData) : null;
|
|
545
|
-
|
|
546
|
-
// Pick the best or requested one
|
|
547
65
|
const useFlz =
|
|
548
66
|
alg === 'flz' || (!alg && flzData && (!cdData || flzData.length < cdData.length));
|
|
549
67
|
|
|
@@ -554,17 +72,37 @@ export const compress_call = function (payload: any, alg?: string): any {
|
|
|
554
72
|
calldata = cdData!;
|
|
555
73
|
bytecode = rleFwdBytecode(to);
|
|
556
74
|
}
|
|
75
|
+
|
|
76
|
+
decompressorAddress = DEC_ADDR;
|
|
77
|
+
fromAddr = from ? _normHex(from).padStart(16, '0') : undefined;
|
|
78
|
+
balanceHex = '0';
|
|
79
|
+
if (
|
|
80
|
+
!alg &&
|
|
81
|
+
jit &&
|
|
82
|
+
jit.bytecode.length + jit.calldata.length < bytecode.length + calldata.length
|
|
83
|
+
) {
|
|
84
|
+
bytecode = jit.bytecode;
|
|
85
|
+
calldata = jit.calldata;
|
|
86
|
+
decompressorAddress = jit.to;
|
|
87
|
+
fromAddr = jit.from;
|
|
88
|
+
balanceHex = jit.balance;
|
|
89
|
+
}
|
|
557
90
|
}
|
|
558
91
|
|
|
559
92
|
// Skip if not beneficial
|
|
560
93
|
if (bytecode.length + calldata.length >= originalSize) return payload;
|
|
561
94
|
|
|
95
|
+
const stateOverride: any = {
|
|
96
|
+
code: bytecode,
|
|
97
|
+
balance: '0x' + balanceHex,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const compressedTxObj: any = { to: decompressorAddress, data: calldata };
|
|
101
|
+
|
|
102
|
+
if (fromAddr) compressedTxObj.from = '0x' + fromAddr;
|
|
103
|
+
|
|
562
104
|
return {
|
|
563
105
|
...payload,
|
|
564
|
-
params: [
|
|
565
|
-
{ ...txObj, to: DECOMPRESSOR_ADDRESS, data: calldata },
|
|
566
|
-
blockParam || 'latest',
|
|
567
|
-
{ ...overrides, [DECOMPRESSOR_ADDRESS]: { code: bytecode } },
|
|
568
|
-
],
|
|
106
|
+
params: [compressedTxObj, blockParam, { ...overrides, [decompressorAddress]: stateOverride }],
|
|
569
107
|
};
|
|
570
108
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eth-compress",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.",
|
|
@@ -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.26.1",
|
|
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.10",
|
|
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.5",
|
|
119
|
+
"@types/node": "25.0.3",
|
|
120
|
+
"esbuild": "0.27.2",
|
|
121
121
|
"typescript": "5.9.3",
|
|
122
|
-
"viem": "2.
|
|
123
|
-
"vitest": "4.0.
|
|
122
|
+
"viem": "2.43.3",
|
|
123
|
+
"vitest": "4.0.16"
|
|
124
124
|
},
|
|
125
125
|
"browserslist": [
|
|
126
126
|
">0.3%",
|