@volga-sh/evm-ghostcall 0.0.3 → 0.0.5

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/src/Ghostcall.yul CHANGED
@@ -1,130 +1,49 @@
1
1
  object "Ghostcall" {
2
2
  code {
3
- // Ghostcall is an "initcode program" rather than a normal deployed contract.
4
- //
5
- // Mental model:
6
- // 1. A normal CREATE transaction executes initcode.
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 caller-facing input invariants ahead of time. The only
46
- // top-level check left here protects response packing, because returndata size is learned
47
- // from the EVM after each CALL.
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.
48
7
 
49
- // dataoffset("user_payload_anchor") is the byte offset of the empty data section declared at
50
- // the bottom of this file. Because that data section is placed after the code, its offset is
51
- // exactly "the first byte after the compiled initcode". That makes it the start of the
52
- // caller-appended payload.
8
+ // The empty trailing data section marks the first caller-appended byte.
53
9
  let payloadCursor := dataoffset("user_payload_anchor")
54
10
 
55
- // Memory layout used by this program:
56
- // - 0x00..writePtr: finalized output buffer that will become the eth_call return value
57
- // - writePtr..writePtr+0x1f: scratch space for reading the current entry header
58
- //
59
- // writePtr always points to where the next result entry starts. The entry's memory is
60
- // scratch until CALL completes, then the packed result overwrites that same region.
11
+ // [0, writePtr) is finalized output. Everything after it is scratch until
12
+ // CALL finishes, then overwritten with the next packed result.
61
13
  let writePtr := 0x00
62
14
 
63
- // Process entries until the cursor reaches the end of the CREATE payload. SDK-generated
64
- // payloads always land exactly on codesize(); raw malformed trailing bytes are outside the
65
- // supported boundary and are not checked here.
66
15
  for {} lt(payloadCursor, codesize()) {} {
67
- // Read the 22-byte fixed-size entry header into scratch memory at writePtr.
68
- //
69
- // The header layout is [len(2)][target(20)]. One mload gives us:
70
- // [2-byte len][20-byte target][10 trailing bytes]
16
+ // One word holds [length(2)][target(20)][unused(10)].
71
17
  codecopy(writePtr, payloadCursor, 0x16)
72
-
73
18
  let headerWord := mload(writePtr)
74
-
75
- // The high 2 bytes hold the big-endian uint16 calldata length. The target occupies the
76
- // next 20 bytes, so shr(80, headerWord) yields the address for CALL.
77
19
  let calldataSize := shr(240, headerWord)
78
20
 
79
- // Put calldata after the 22-byte input header scratch. The returned entry later uses
80
- // only writePtr..writePtr+0x01 for its packed header and writePtr+0x02 onward for
81
- // returndata, so this staging area can be safely overwritten after CALL.
21
+ // Stage calldata after the input header; returndata later overwrites it.
82
22
  let calldataPtr := add(writePtr, 0x16)
83
23
  let returndataPtr := add(writePtr, 0x02)
84
-
85
- // Copy just this call's calldata into memory so CALL can read it.
86
24
  codecopy(calldataPtr, add(payloadCursor, 0x16), calldataSize)
87
25
 
88
- // Execute the external call with:
89
- // - all remaining gas
90
- // - zero ETH value
91
- // - calldata in memory at calldataPtr
92
- // - no output buffer yet, because we do not know returndata size in advance
93
- //
26
+ // CALL truncates the shifted word to the low 160 address bits.
27
+ // Zero-value CALL (not STATICCALL) exposes state changes to later calls.
94
28
  let success := call(gas(), shr(80, headerWord), 0, calldataPtr, calldataSize, 0, 0)
95
29
  let returndataSize := returndatasize()
96
30
 
97
- // The packed result header has 15 returndata length bits; bit 15 is the success flag.
98
- // Revert rather than letting oversized returndata collide with the success bit.
31
+ // Reject lengths that would collide with the success bit.
99
32
  if shr(15, returndataSize) {
100
33
  revert(0x00, 0x00)
101
34
  }
102
35
 
103
- // Intentionally do not enforce an aggregate response-size cap here. CREATE-style
104
- // execution already treats returned bytes as would-be runtime code, so the active
105
- // chain/client/RPC environment will reject oversized responses according to its own
106
- // code-size policy. Keeping this uncapped lets the same Ghostcall initcode benefit from
107
- // networks with larger limits, such as Monad's MIP-2:
108
- // https://mips.monad.xyz/MIPS/MIP-2
109
-
110
- // Write the packed 2-byte result header into the high 2 bytes of the 32-byte word at
111
- // writePtr. The rest of that word does not matter because the return length is computed
112
- // 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.
113
38
  mstore(writePtr, shl(240, or(shl(15, success), returndataSize)))
114
-
115
- // Append the raw returndata bytes immediately after the 2-byte header.
116
39
  returndatacopy(returndataPtr, 0, returndataSize)
117
40
 
118
- // Advance both cursors:
119
- // - writePtr moves to the start of the next result entry
120
- // - payloadCursor moves to the next input entry
121
41
  writePtr := add(returndataPtr, returndataSize)
122
42
  payloadCursor := add(payloadCursor, add(0x16, calldataSize))
123
43
  }
124
44
 
125
- // Return exactly the bytes that were written to the response buffer.
45
+ // Aggregate size is governed by the active chain/client's CREATE policy.
126
46
  return(0x00, writePtr)
127
-
128
47
  }
129
48
 
130
49
  data "user_payload_anchor" hex""