@volga-sh/evm-ghostcall 0.0.2 → 0.0.4
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 +51 -28
- package/dist/sdk/abi.d.ts +39 -0
- package/dist/sdk/abi.d.ts.map +1 -0
- package/dist/sdk/abi.js +20 -0
- package/dist/sdk/abi.js.map +1 -0
- package/dist/sdk/generated/initcode.d.ts +1 -1
- package/dist/sdk/generated/initcode.d.ts.map +1 -1
- package/dist/sdk/generated/initcode.js +1 -1
- package/dist/sdk/generated/initcode.js.map +1 -1
- package/dist/sdk/index.d.ts +55 -282
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +70 -308
- package/dist/sdk/index.js.map +1 -1
- package/package.json +5 -4
- package/src/Ghostcall.yul +28 -131
package/src/Ghostcall.yul
CHANGED
|
@@ -1,152 +1,49 @@
|
|
|
1
1
|
object "Ghostcall" {
|
|
2
2
|
code {
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// 2. That initcode usually builds runtime bytecode and RETURNs it.
|
|
8
|
-
// 3. Ghostcall uses the same mechanism, but inside eth_call.
|
|
9
|
-
// 4. Because this is only a simulation, nothing is deployed.
|
|
10
|
-
// 5. Whatever bytes this program RETURNs become the eth_call result.
|
|
11
|
-
//
|
|
12
|
-
// In other words: Ghostcall treats CREATE initcode like a tiny one-shot program that can
|
|
13
|
-
// batch external CALLs and return their raw results.
|
|
14
|
-
//
|
|
15
|
-
// The caller sends one byte blob:
|
|
16
|
-
// <compiled ghostcall initcode><payload>
|
|
17
|
-
//
|
|
18
|
-
// The payload is appended directly after the compiled initcode. It is not normal calldata.
|
|
19
|
-
// This program reads that appended payload back out of its own code using CODECOPY.
|
|
20
|
-
//
|
|
21
|
-
// Payload layout:
|
|
22
|
-
// repeated call entries
|
|
23
|
-
//
|
|
24
|
-
// Each call entry:
|
|
25
|
-
// 2 bytes calldata length (big-endian uint16)
|
|
26
|
-
// 20 bytes target address
|
|
27
|
-
// N bytes calldata
|
|
28
|
-
//
|
|
29
|
-
// Output layout:
|
|
30
|
-
// repeated result entries
|
|
31
|
-
//
|
|
32
|
-
// Each result entry:
|
|
33
|
-
// 2 bytes packed header
|
|
34
|
-
// bit 15 = success flag from CALL
|
|
35
|
-
// bits 0-14 = returndata length (big-endian uint15)
|
|
36
|
-
// N bytes returndata
|
|
37
|
-
//
|
|
38
|
-
// The program does the same high-level loop for every entry:
|
|
39
|
-
// - read the next calldata length + target
|
|
40
|
-
// - copy that call's calldata into memory
|
|
41
|
-
// - execute CALL(target, calldata)
|
|
42
|
-
// - append (success, returndata) to the response buffer
|
|
43
|
-
// - continue until the payload is fully consumed
|
|
44
|
-
//
|
|
45
|
-
// The SDK is expected to validate most caller-facing invariants ahead of time. The checks
|
|
46
|
-
// left in this file exist only to protect parser correctness and response packing.
|
|
3
|
+
// CREATE-style eth_call initcode: RETURN becomes the simulated runtime bytes.
|
|
4
|
+
// Input: appended [uint16 calldata length][20-byte target][calldata] entries.
|
|
5
|
+
// Output: [success bit | uint15 returndata length][returndata] entries.
|
|
6
|
+
// The SDK validates inputs; malformed hand-built payloads are unsupported.
|
|
47
7
|
|
|
48
|
-
//
|
|
49
|
-
// the bottom of this file. Because that data section is placed after the code, its offset is
|
|
50
|
-
// exactly "the first byte after the compiled initcode". That makes it the start of the
|
|
51
|
-
// caller-appended payload.
|
|
8
|
+
// The empty trailing data section marks the first caller-appended byte.
|
|
52
9
|
let payloadCursor := dataoffset("user_payload_anchor")
|
|
53
10
|
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
// writePtr always points to "where the next result entry should be written".
|
|
59
|
-
let writePtr := 0x20
|
|
11
|
+
// [0, writePtr) is finalized output. Everything after it is scratch until
|
|
12
|
+
// CALL finishes, then overwritten with the next packed result.
|
|
13
|
+
let writePtr := 0x00
|
|
60
14
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Read the 22-byte fixed-size entry header into scratch memory starting at 0x0a rather
|
|
68
|
-
// than 0x00.
|
|
69
|
-
//
|
|
70
|
-
// Why 0x0a?
|
|
71
|
-
// - the header layout is [len(2)][target(20)]
|
|
72
|
-
// - placing the first header byte at memory offset 10 makes the 20-byte target end
|
|
73
|
-
// exactly at byte 31 of the 32-byte word loaded from mload(0x00)
|
|
74
|
-
// - that means one mload gives us:
|
|
75
|
-
// [10 zero bytes][2-byte len][20-byte target]
|
|
76
|
-
// - so shr(160, headerWord) yields calldata length
|
|
77
|
-
// - and headerWord itself already has the target in the low 20 bytes for CALL
|
|
78
|
-
//
|
|
79
|
-
// CODECOPY pads with zeros if it reads past the end of code. That is why we still need
|
|
80
|
-
// an explicit bounds check later: without it, a truncated entry would silently decode as
|
|
81
|
-
// zeros instead of failing.
|
|
82
|
-
codecopy(0x0a, payloadCursor, 0x16)
|
|
83
|
-
|
|
84
|
-
let headerWord := mload(0x00)
|
|
85
|
-
|
|
86
|
-
// The high 2 non-zero bytes hold the big-endian uint16 calldata length.
|
|
87
|
-
let calldataSize := shr(160, headerWord)
|
|
88
|
-
let nextCursor := add(add(payloadCursor, 0x16), calldataSize)
|
|
89
|
-
|
|
90
|
-
// Reject truncated entries. This single check covers both:
|
|
91
|
-
// - not enough bytes for the 22-byte header
|
|
92
|
-
// - not enough bytes for the calldata that the header claims exists
|
|
93
|
-
if gt(nextCursor, codesize()) {
|
|
94
|
-
revert(0x00, 0x00)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// The next result entry will be written at writePtr. Its first 2 bytes are the packed
|
|
98
|
-
// header, so the calldata scratch area can safely start immediately after that header.
|
|
99
|
-
let calldataPtr := add(writePtr, 0x02)
|
|
15
|
+
for {} lt(payloadCursor, codesize()) {} {
|
|
16
|
+
// One word holds [length(2)][target(20)][unused(10)].
|
|
17
|
+
codecopy(writePtr, payloadCursor, 0x16)
|
|
18
|
+
let headerWord := mload(writePtr)
|
|
19
|
+
let calldataSize := shr(240, headerWord)
|
|
100
20
|
|
|
101
|
-
//
|
|
21
|
+
// Stage calldata after the input header; returndata later overwrites it.
|
|
22
|
+
let calldataPtr := add(writePtr, 0x16)
|
|
23
|
+
let returndataPtr := add(writePtr, 0x02)
|
|
102
24
|
codecopy(calldataPtr, add(payloadCursor, 0x16), calldataSize)
|
|
103
25
|
|
|
104
|
-
//
|
|
105
|
-
// -
|
|
106
|
-
|
|
107
|
-
// - calldata in memory at calldataPtr
|
|
108
|
-
// - no output buffer yet, because we do not know returndata size in advance
|
|
109
|
-
//
|
|
110
|
-
// CALL only cares about the low 20 bytes of its address argument, so headerWord can be
|
|
111
|
-
// passed directly: the target is already sitting there after the 0x0a codecopy trick.
|
|
112
|
-
let success := call(gas(), headerWord, 0, calldataPtr, calldataSize, 0, 0)
|
|
26
|
+
// CALL truncates the shifted word to the low 160 address bits.
|
|
27
|
+
// Zero-value CALL (not STATICCALL) exposes state changes to later calls.
|
|
28
|
+
let success := call(gas(), shr(80, headerWord), 0, calldataPtr, calldataSize, 0, 0)
|
|
113
29
|
let returndataSize := returndatasize()
|
|
114
30
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
if gt(returndataSize, 0x7fff) {
|
|
31
|
+
// Reject lengths that would collide with the success bit.
|
|
32
|
+
if shr(15, returndataSize) {
|
|
118
33
|
revert(0x00, 0x00)
|
|
119
34
|
}
|
|
120
35
|
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
let nextWritePtr := add(add(writePtr, 0x02), returndataSize)
|
|
124
|
-
|
|
125
|
-
// Intentionally do not enforce an aggregate response-size cap here. CREATE-style
|
|
126
|
-
// execution already treats returned bytes as would-be runtime code, so the active
|
|
127
|
-
// chain/client/RPC environment will reject oversized responses according to its own
|
|
128
|
-
// code-size policy. Keeping this uncapped lets the same Ghostcall initcode benefit from
|
|
129
|
-
// networks with larger limits, such as Monad's MIP-2:
|
|
130
|
-
// https://mips.monad.xyz/MIPS/MIP-2
|
|
131
|
-
|
|
132
|
-
// Write the packed 2-byte result header into the high 2 bytes of the 32-byte word at
|
|
133
|
-
// writePtr. The rest of that word does not matter because the return length is computed
|
|
134
|
-
// explicitly at the end.
|
|
36
|
+
// Put the header in the high two bytes; only the final written length
|
|
37
|
+
// is returned, so the remainder of this word may be overwritten freely.
|
|
135
38
|
mstore(writePtr, shl(240, or(shl(15, success), returndataSize)))
|
|
39
|
+
returndatacopy(returndataPtr, 0, returndataSize)
|
|
136
40
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// Advance both cursors:
|
|
141
|
-
// - writePtr moves to the start of the next result entry
|
|
142
|
-
// - payloadCursor moves to the next input entry
|
|
143
|
-
writePtr := nextWritePtr
|
|
144
|
-
payloadCursor := nextCursor
|
|
41
|
+
writePtr := add(returndataPtr, returndataSize)
|
|
42
|
+
payloadCursor := add(payloadCursor, add(0x16, calldataSize))
|
|
145
43
|
}
|
|
146
44
|
|
|
147
|
-
//
|
|
148
|
-
return(
|
|
149
|
-
|
|
45
|
+
// Aggregate size is governed by the active chain/client's CREATE policy.
|
|
46
|
+
return(0x00, writePtr)
|
|
150
47
|
}
|
|
151
48
|
|
|
152
49
|
data "user_payload_anchor" hex""
|