diff --git a/EIPS/eip-9999.md b/EIPS/eip-9999.md index e853cff519485d..3b2dae81db4b26 100644 --- a/EIPS/eip-9999.md +++ b/EIPS/eip-9999.md @@ -48,11 +48,11 @@ This EIP solves this problem by adding **STARK-based aggregation**. Instead of a ### New Frame Types -This EIP introduces two new frame modes for [EIP-8141](./eip-8141.md) frame transactions: +This EIP introduces one new frame mode for [EIP-8141](./eip-8141.md) frame transactions: #### Dependency Verification Frame (`DEP_VERIFY_FRAME_MODE`) -We modify EIP-8141 to also allow frames with mode `DEP_VERIFY_FRAME_MODE`, in addition to all previously allowed modes. A frame with mode `DEP_VERIFY_FRAME_MODE` is a **dependency verification frame**. It declares a set of **dependencies** as `(scheme, data_hash, verification_key)` triples. These triples are not executed as EVM code; instead, they are recorded as dependencies that must be proven valid by the recursive STARK in the block. +We modify EIP-8141 to also allow frames with mode `DEP_VERIFY_FRAME_MODE`, in addition to all previously allowed modes. Concretely, EIP-8141's per-frame transaction-validity rule `assert frame.mode < 3` is amended to `assert frame.mode < 4` (equivalently `frame.mode <= DEP_VERIFY_FRAME_MODE`), so a dependency verification frame is not rejected as an unknown mode. A frame with mode `DEP_VERIFY_FRAME_MODE` is a **dependency verification frame**. It declares a set of **dependencies** as `(scheme, data_hash, verification_key)` triples. These triples are not executed as EVM code; instead, they are recorded as dependencies that must be proven valid by the recursive STARK in the block. The **payload encoding** is a simple `96*k` byte concatenation of `k` triples, each consisting of: @@ -99,9 +99,9 @@ dependencies(block) = [ ### Transaction Execution Visibility -During EVM execution, the transaction's dependencies are made available through the existing [EIP-8141](./eip-8141.md) `FRAMEPARAM`, `FRAMEDATASIZE`, and `FRAMEDATACOPY` instructions. +During EVM execution, the transaction's dependencies are made available through the existing [EIP-8141](./eip-8141.md) `FRAMEPARAM` and `FRAMEDATACOPY` instructions. -Contracts can iterate over all frames in the transaction using `FRAMEPARAM` to identify `DEP_VERIFY_FRAME_MODE` frames, then use `FRAMEDATASIZE` and `FRAMEDATACOPY` to read the dependency data from each frame's `data` field. +Contracts can iterate over all frames in the transaction using `FRAMEPARAM` to identify `DEP_VERIFY_FRAME_MODE` frames, then use `FRAMEPARAM(0x04, i)` (length) and `FRAMEDATACOPY` to read the dependency data from each frame's `data` field. Specifically: @@ -133,7 +133,7 @@ recursive_stark = [stark_proof, block_deps_hash] ``` block_deps_hash = hash(concat([ int_to_bytes32_big_endian(t.scheme) + t.data_hash + t.verification_key - for t in get_dependencies(block) + for t in dependencies(block) ])) ``` @@ -174,10 +174,10 @@ for i in range(len(recursive_proofs)): all_deps.extend(inner_deps) # Remove discards and duplicates -discard_hashes = set(get_deps_hash([d]) for d in discards) +discard_set = set(discards) filtered_deps = [] for dep in all_deps: - if dep not in filtered_deps and dep not in discard_hashes: + if dep not in filtered_deps and dep not in discard_set: filtered_deps.append(dep) # Verify final deps hash matches public input @@ -200,10 +200,12 @@ To enable unlimited-depth recursion, we need to pass the `AGGREGATED_VK` in as a A block is valid if and only if: -1. The `block_deps_hash` in the recursive STARK header entry matches the result of hashing `get_dependencies(block)` +1. The `block_deps_hash` in the recursive STARK header entry matches the result of hashing `dependencies(block)` 2. The recursive STARK proof verifies successfully against `block_deps_hash` and the fixed protocol-level `AGGREGATED_VK`. 3. All transactions satisfy the normal (EIP-8141 or other) validity rules. +`AGGREGATED_VK` is a fixed protocol constant and is never chosen by users. It is passed as a public input to enable unlimited-depth recursion, but block validity rule 2 pins the outer proof to the protocol value, and the identical `AGGREGATED_VK` must be used as the verification key and public input for every nested recursive proof at every depth (this is why a recursive proof's public inputs are `[deps_hash, AGGREGATED_VK]`, whereas a leaf verification takes only `data_hash`). The per-dependency leanSTARK verification keys inside the `(scheme, data_hash, verification_key)` triples, referenced in Security Considerations, are distinct from `AGGREGATED_VK`. + ### Mempool Wrapper Object At the mempool level, transactions with dependency verification frames are wrapped in **wrapper objects** that are broadcast once per `AGGREGATION_INTERVAL`. Each wrapper contains a **list of transactions** and either their direct dependencies or a single recursive STARK proving all dependencies. @@ -220,7 +222,7 @@ content = if mode == 0: [deps, proofs] Where: -- `transactions` = list of transactions in the wrapper. For transactions that were already broadcast in a previous wrapper, their hash is used instead of the full transaction to save space. +- `transactions` = list of transactions in the wrapper. For transactions that were already broadcast in a previous wrapper, their hash is used instead of the full transaction to save space. A node can validate a wrapper only once it holds the full transaction bytes for every referenced transaction (required to recompute each transaction's dependencies); a wrapper with an unresolved transaction hash is not yet validatable or forwardable. - `deps` = concatenation of all `(scheme, data_hash, verification_key)` triples from all transactions in the wrapper - `proofs` = list of individual proofs corresponding to each dependency in `deps` - `recursive_stark` = a single recursive STARK proving validity of all deps, with public input `hash(deps)` @@ -234,10 +236,11 @@ Where: **Mode 1 (Recursive STARK)**: When a mempool node receives a wrapper with `mode == 1`, they must verify that: -1. The recursive STARK's public input `deps_hash` equals `hash(deps)` -2. The recursive STARK is valid (proves all deps are valid) -3. The total number of leanSPHINCS deps is `<= MAX_LEANSIG_DEPS_PER_WRAPPER` -4. The total number of leanSTARK deps is `<= MAX_LEANSTARK_DEPS_PER_WRAPPER` +1. The `deps` list is the concatenation of all dependencies from each transaction in `transactions` (identical to the mode 0 check) +2. The recursive STARK's public input `deps_hash` equals `hash(deps)` +3. The recursive STARK is valid (proves all deps are valid) +4. The total number of leanSPHINCS deps is `<= MAX_LEANSIG_DEPS_PER_WRAPPER` +5. The total number of leanSTARK deps is `<= MAX_LEANSTARK_DEPS_PER_WRAPPER` Each wrapper contains exactly one STARK in mode 1, and one STARK per leanSTARK dependency in mode 0. Per `AGGREGATION_INTERVAL`, a mempool node broadcasts exactly one wrapper containing all currently active transactions, with exactly one STARK covering all their dependencies. @@ -264,6 +267,8 @@ If a node anticipates that it will not have enough proving time to aggregate in Nodes may also alternate between adding one object at a time from (3) and (4). +If a node cannot produce a new recursive wrapper before the next aggregation interval, it SHOULD forward the valid wrappers it received unchanged, rather than blocking propagation. Recursive folding is a bandwidth optimization, not a precondition for forwarding: a node always verifies a wrapper before forwarding it (per the mode 0 / mode 1 rules above), but a node that cannot fold in time still propagates what it has already verified. + #### Mempool-Level Limits To prevent DoS attacks, the following mempool-level limits apply to recursive STARK aggregation: