Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
543 changes: 543 additions & 0 deletions docs/superpowers/plans/2026-06-03-poseidon-gencontract-memory-state.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Design: Poseidon Contract Generator — Memory-Based State for nInputs up to 16

**Date:** 2026-06-03
**Status:** Approved
**File:** `src/poseidon_gencontract.js`

---

## Problem

`createCode(nInputs)` generates EVM bytecode for the Poseidon hash function. The `mix()` function keeps both old and new state simultaneously on the EVM stack, requiring `DUP` instructions up to index `2t - 1` where `t = nInputs + 1`. EVM caps DUP at index 15 (DUP16), limiting the generator to `nInputs ≤ 6` in practice (despite the guard claiming 1–8). `createCode(7)` and `createCode(8)` both throw at runtime.

The goal is to support `nInputs` up to 16 in a single contract with no cross-contract call overhead.

---

## Approach: Memory-Based State Management (single contract)

Move the Poseidon state out of the EVM stack and into EVM memory. The stack holds only `q` (the BN254 field prime) throughout the entire computation. DUP indices never exceed 4 anywhere in the generated bytecode.

---

## Memory Layout

For `t = nInputs + 1`:

| Region | Byte offset | Size |
|---|---|---|
| State `s[0..t-1]` | `0` | `t × 32` |
| Return address for `mix()` | `t × 32` | `32` |
| MDS matrix `M[i][j]` | `(t+1) × 32` | `t² × 32` |
| New-state temp buffer | `(t+1+t²) × 32` | `t × 32` |

For `t = 17` (nInputs=16): ~10.3 KB total — well within EVM memory limits.

`state[0]` is the capacity element (always 0). `state[1..nInputs]` hold the inputs. Because `stateOffset = 0`, the result `state[0]` is already at `memory[0]` after all rounds — the final `RETURN` can be issued without an extra `MSTORE`.

---

## Rewritten Functions

### `saveM()`

Writes `M[t-2][i][j]` to `memory[matrixOffset + (i*t + j) * 32]` where `matrixOffset = (t+1) * 32`. Same logic as today; only the target offset changes.

### Initial setup

```
push q // [q]
push 0; push 0; mstore // state[0] = 0 (capacity)
for i in 0..nInputs:
push(0x04 + i*0x20); calldataload // [input[i], q]
push(stateOffset + (i+1)*32); mstore // [q]
```

Stack stays `[q]`. No state on the stack.

### `ark(r)`

For each `i` in `0..t-1`:
```
dup(0) // [q, q]
push(stateOffset + i*32); mload // [s[i], q, q]
push(K[t-2][r*t+i]) // [K, s[i], q, q]
swap(1) // [s[i], K, q, q]
addmod // [(s[i]+K)%q, q]
push(stateOffset + i*32); mstore // [q]
```

Max stack depth: 4.

### `sigma(p)`

```
push(stateAddr); mload // [st, q]
dup(0) // [st, st, q]
mulmod // [st², q] (a=st, b=st, N=q)
dup(0) // [st², st², q]
mulmod // [st⁴, q]
push(stateAddr); mload // [st, st⁴, q] (reload original st)
swap(1) // [st⁴, st, q]
mulmod // [st⁵, q]
push(stateAddr); mstore // [q]
```

Max stack depth: 3. Uses one extra `MLOAD` to retrieve `st` for the final multiply (avoids needing it on the stack across the first two mulmods).

### `mix()`

For each output row `i` in `0..t-1`:

```
push 0 // accumulator = 0; [0, q]
for j in 0..t-1:
dup(1) // [q, acc, q]
push(stateOffset + j*32); mload // [s[j], q, acc, q]
push(matrixOffset + (i*t+j)*32); mload // [M, s[j], q, acc, q]
swap(1) // [s[j], M, q, acc, q]
mulmod // [prod, acc, q]
addmod // [new_acc, q]
push(newStateOffset + i*32); mstore // [q]
```

After all rows, copy newState → state:
```
for i in 0..t-1:
push(newStateOffset + i*32); mload // [v, q]
push(stateOffset + i*32); mstore // [q]
```

Jump to return address:
```
push(retAddrOffset); mload; jmp
```

Max stack depth: **5**. No DUP index exceeds 1. DUP limit is no longer a constraint at any `t`.

### Final return

```
push 0x20
push 0x00
return // returns memory[0..31] = state[0] = result
```

---

## Constants Extension

`poseidon_constants.js` currently covers `t = 2..9`. Supporting `nInputs` up to 16 requires extending to `t = 2..17`.

**Two artifacts to extend:**

1. **`N_ROUNDS_P`** — add 8 entries for `t = 10..17`. Values come from the Poseidon paper (Table 2, BN254 prime, α=5, `n_F = 8`). Must be verified against the official Sage generation scripts before use.

2. **`poseidon_constants.json`** (and the JS re-export `poseidon_constants.js`) — add `C` (round constants) and `M` (MDS matrices) entries for each new `t`. Generation process: run the Poseidon reference parameter scripts (Sage or Python) for the BN254 field at each new `t`, then append to the existing JSON structure. The existing `tools/poseidon_optimize_constants.js` handles the JS-side constant optimization and can be extended for the new `t` values.

**Note:** `poseidon_constants_opt.js` is used by the JS Poseidon implementation (`poseidon_opt.js`), not by the contract generator. It should also be extended for consistency but is not on the critical path for contract generation.

---

## Input validation change

```js
// Before
if ((nInputs < 1) || (nInputs > 8)) throw ...
// After
if ((nInputs < 1) || (nInputs > 16)) throw ...
```

`N_ROUNDS_P` index access changes from `N_ROUNDS_P[t - 2]` (unchanged) but the array must now have 16 entries.

---

## Testing

- Add contract deployment + hash correctness tests for `nInputs = 7..16` in `test/poseidoncontract.js`, following the existing pattern for `nInputs = 1..6`.
- Verify bytecode hashes for each new `nInputs` (add `assert.equal(keccak256(code), "0x...")` once constants are finalized).
- Existing `nInputs = 1..6` tests must continue to pass unchanged — the memory layout is a non-breaking internal change since the ABI and selector logic are unaffected.

---

## What does NOT change

- ABI generation (`generateABI`) — unchanged.
- Function selector check and calldata loading logic — unchanged.
- `evmasm.js` — no changes needed; all DUP/SWAP calls stay within existing limits.
- Gas cost model: memory expansion cost for ~10 KB is ~3000 gas at current EVM pricing, paid once per call. No cross-contract call overhead.
11 changes: 10 additions & 1 deletion hardhat.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,14 @@ require("@nomicfoundation/hardhat-ethers");
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.7.3"
solidity: "0.8.35",
networks: {
hardhat: {
hardfork: "osaka", // Fusaka (osaka in hardhat) hard fork
// Generated Poseidon bytecode exceeds the 24576-byte EIP-170 mainnet limit for nInputs >= 6
// EIP-7907 (In Draft now), which proposes increasing Ethereum's contract code size limit, is slated to be introduced in the Glamsterdam hard fork
// We need to set `allowUnlimitedContractSize` to true in order to deploy the Poseidon library on hardhat's local network
allowUnlimitedContractSize: true,
},
},
};
Loading
Loading