fix(l1): close EIP-8025 stateless witness validation gaps (re-enable 9 zkevm tests)#6541
fix(l1): close EIP-8025 stateless witness validation gaps (re-enable 9 zkevm tests)#6541avilagaston9 wants to merge 14 commits intomainfrom
Conversation
…5.6.1 base, so it lands cleanly on main) and widen the Makefile zkevm-vectors extraction from just eip8025_optional_proofs to the whole for_amsterdam tree. This lifts witness-based stateless coverage from the ~93 eip8025 fixtures to all 2763 for_amsterdam fixtures, since every zkevm fixture now carries executionWitness. Order the zkevm extraction after amsterdam-vectors so its witness-bearing JSONs overlay the non-witness copies produced by bal@v5.6.1, giving us the "free extra checks" the EEST maintainers suggested. Teach run_stateless_from_fixture about the zkevm output format. The fixture's statelessOutputBytes packs new_payload_request_root (32 B) || valid (1 B) || padding, so byte 32 is the expected validity marker. parse_expected_valid_flag decodes it and the outcome match now accepts two cases as success: valid=1 with Ok and valid=0 with Err (the new tolerance/rejection tests rely on the latter). Without this, every zkevm@v0.3.3 "invalid executionWitness" test would fail with an inverted error. Refactor the skip list in tests/all.rs so prover/stateless extras are feature-gated instead of living in SKIPPED_BASE. Add a new #[cfg(feature = "stateless")] EXTRA_SKIPS group covering nine validation_* tests the v0.3.3 bundle exposes: four "tolerance" cases (valid=1) where RpcExecutionWitness::into_execution_witness eagerly rejects entries the execution path never touches, and five "rejection" cases (valid=0) where our stateless path runs clean instead of noticing a missing code or ancestor entry. Both groups have TODOs pointing at the real follow-up work (lazy witness parsing and completeness checks in the guest program). End state: test-stateless goes from 16 failures to 8720 passed / 0 failed / 9 skipped. test-levm behavior is unchanged.
so the resulting list satisfies `headers[i].parent_hash == keccak(headers[i-1])`, matching the EELS contract for `validate_headers`. The generator walked the chain backward (newest -> oldest), so reverse the byte list before returning. This is a no-op for current consumers because the headers are stored in a `BTreeMap<u64, BlockHeader>` keyed by number, but it makes the witness valid for any spec-conformant stateless verifier and is a precondition for adding the EIP-8025 contiguity check on the consumer side. Reference: https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/stateless.py#L171-L191
…teness: - Tolerance: when decoding state nodes and ancestor headers in `into_execution_witness` and `from_witness`, drop entries that fail to RLP-decode instead of failing the whole conversion. A bad/extra entry cannot be looked up by hash anyway; if the trie walk or BLOCKHASH path actually requires the dropped entry, the lookup fails explicitly there. Mirrors EELS `witness_state.build_node_db` and geth `MakeHashDB`, which both store entries keyed by hash without pre-validation. - Header chain contiguity: in `from_witness`, walk the header byte list in order and reject when `headers[i].parent_hash != keccak(headers[i-1])`. A reordered or fragmented header chain is not a valid witness even if the by-number lookup would otherwise resolve to the right header. Mirrors EELS `stateless.validate_headers`. Malformed entries are treated as a chain break (subsequent headers won't satisfy the parent_hash check), preserving the tolerance behavior for blocks that do not actually request the bad header. - Codes completeness: `get_account_code` and `get_code_metadata` now error on missing bytecode instead of silently defaulting to empty code. EIP-8025 mandates that a stateless executor reaching a code lookup whose hash is not in the witness treat the witness as incomplete and reject. Matches EELS `witness_state.get_code` (raises KeyError on miss) and geth's documented "bytecode lookup will error on junk" model. References: - https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/witness_state.py#L37-L42 - https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/witness_state.py#L204-L212 - https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/stateless.py#L171-L191 - https://github.com/ethereum/go-ethereum/blob/master/core/stateless/database.go#L26-L67
…ed under `feature = "stateless"`. They now pass with the witness-consumer alignment (tolerance + contiguity + codes-completeness) and the generator-side ascending header order: - validation_state_extra_unused_trie_node - validation_headers_malformed_rlp_header - validation_headers_missing_oldest_blockhash_ancestor - validation_headers_missing_parent_header - validation_headers_non_contiguous_chain - validation_codes_missing_delegated_code_on_insufficient_balance_call - validation_codes_missing_external_code_read_target - validation_codes_missing_redelegation_old_marker - validation_codes_missing_sender_delegation_marker `make test-stateless`: 8720 passed / 0 failed / 0 ignored (~155 s). `make test-levm`: unchanged.
…; keep the spec link but drop the prose. No behavior change.
Lines of code reportTotal lines added: Detailed view |
…nversion comments — keep the prose and the spec links, just remove the symbol-plus-word labels. Restore the `This is an optimized path for EXTCODESIZE opcode.` doc line on `get_code_metadata` that was dropped while trimming. No behavior change.
…ader.parent_hash != expected_parent { ... } }` in `from_witness` into a single let-chain `if`. Required to satisfy `clippy::collapsible_if` under `-D warnings` on rust 1.91 — the CI Lint and Lint L2 jobs were failing on this. Behavior unchanged.
Greptile SummaryThis PR closes four EIP-8025 stateless-witness validation gaps, re-enabling 9 previously-skipped Confidence Score: 4/5Safe to merge; only P2 style issues found, core logic is correct and all targeted tests pass. All findings are P2 (a misleading comment and using crates/common/types/block_execution_witness.rs — comment accuracy and error variant usage in
|
| Filename | Overview |
|---|---|
| crates/common/types/block_execution_witness.rs | Four EIP-8025 changes: tolerance for malformed trie nodes/headers, contiguous-chain validation in from_witness, and hard errors on missing bytecode. Minor: a misleading comment on the malformed-entry path and a Custom error where the typed NoncontiguousBlockHeaders variant already exists. |
| crates/blockchain/blockchain.rs | Adds block_headers_bytes.reverse() in both generate_witness_for_blocks code paths so the emitted ancestor list is ascending, satisfying the new contiguity check in from_witness. |
| tooling/ef_tests/blockchain/tests/all.rs | Removes the stateless-specific skip list (9 tests) and simplifies to a single #[cfg(not(feature = "sp1"))] empty slice, re-enabling all formerly-skipped validation tests. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["RpcExecutionWitness\n(from RPC / fixture)"] -->|"into_execution_witness()"| B["ExecutionWitness\n(block_headers_bytes in ascending order)"]
GEN["generate_witness_for_blocks\n(walks chain backward)"] -->|"reverse() → ascending"| B
B -->|"from_witness()"| C{For each header bytes}
C -->|"decode fails"| D["prev_hash = None\nskip (tolerance)"]
D --> C
C -->|"decode ok\nprev_hash != None AND\nheader.parent_hash != prev_hash"| E["❌ NoncontiguousBlockHeaders\nerror"]
C -->|"decode ok\nchain link valid"| F["insert into block_headers\nprev_hash = keccak(bytes)"]
F --> C
C -->|"done"| G["GuestProgramState"]
G --> H["get_account_code(hash)"]
H -->|"hash missing in codes_hashed"| I["❌ Database error\n(was: silent empty code)"]
H -->|"hash found"| J["✅ Return Code"]
Prompt To Fix All With AI
This is a comment left during a code review.
Path: crates/common/types/block_execution_witness.rs
Line: 330-334
Comment:
**Misleading inline comment — check is skipped, not failed**
The comment says "the next parent_hash check fails," but when `prev_hash` is set to `None`, the subsequent `if let Some(expected_parent) = prev_hash` guard in the next iteration simply does not fire — the check is bypassed, not triggered-and-failed. A validator reading this could mistake the skipping for active rejection.
```suggestion
let Ok(header) = BlockHeader::decode(bytes.as_ref()) else {
// Malformed entry: skip it and reset the chain cursor so the
// next decodable header is accepted without a parent-hash check.
prev_hash = None;
continue;
};
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: crates/common/types/block_execution_witness.rs
Line: 337-340
Comment:
**Use the existing `NoncontiguousBlockHeaders` error variant**
There is already a typed variant `GuestProgramStateError::NoncontiguousBlockHeaders` (defined a few lines above) with the message "Non-contiguous block headers (there's a gap in the block headers list)". Using `Custom(...)` here bypasses that variant and makes error matching on the call-site harder.
```suggestion
{
return Err(GuestProgramStateError::NoncontiguousBlockHeaders);
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Collapse the nested `if let Some(expecte..." | Re-trigger Greptile
| let Ok(header) = BlockHeader::decode(bytes.as_ref()) else { | ||
| // Malformed entry is a chain break; the next parent_hash check fails. | ||
| prev_hash = None; | ||
| continue; | ||
| }; |
There was a problem hiding this comment.
Misleading inline comment — check is skipped, not failed
The comment says "the next parent_hash check fails," but when prev_hash is set to None, the subsequent if let Some(expected_parent) = prev_hash guard in the next iteration simply does not fire — the check is bypassed, not triggered-and-failed. A validator reading this could mistake the skipping for active rejection.
| let Ok(header) = BlockHeader::decode(bytes.as_ref()) else { | |
| // Malformed entry is a chain break; the next parent_hash check fails. | |
| prev_hash = None; | |
| continue; | |
| }; | |
| let Ok(header) = BlockHeader::decode(bytes.as_ref()) else { | |
| // Malformed entry: skip it and reset the chain cursor so the | |
| // next decodable header is accepted without a parent-hash check. | |
| prev_hash = None; | |
| continue; | |
| }; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/types/block_execution_witness.rs
Line: 330-334
Comment:
**Misleading inline comment — check is skipped, not failed**
The comment says "the next parent_hash check fails," but when `prev_hash` is set to `None`, the subsequent `if let Some(expected_parent) = prev_hash` guard in the next iteration simply does not fire — the check is bypassed, not triggered-and-failed. A validator reading this could mistake the skipping for active rejection.
```suggestion
let Ok(header) = BlockHeader::decode(bytes.as_ref()) else {
// Malformed entry: skip it and reset the chain cursor so the
// next decodable header is accepted without a parent-hash check.
prev_hash = None;
continue;
};
```
How can I resolve this? If you propose a fix, please make it concise.| { | ||
| return Err(GuestProgramStateError::Custom( | ||
| "witness headers are not contiguous".to_string(), | ||
| )); |
There was a problem hiding this comment.
Use the existing
NoncontiguousBlockHeaders error variant
There is already a typed variant GuestProgramStateError::NoncontiguousBlockHeaders (defined a few lines above) with the message "Non-contiguous block headers (there's a gap in the block headers list)". Using Custom(...) here bypasses that variant and makes error matching on the call-site harder.
| { | |
| return Err(GuestProgramStateError::Custom( | |
| "witness headers are not contiguous".to_string(), | |
| )); | |
| { | |
| return Err(GuestProgramStateError::NoncontiguousBlockHeaders); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/types/block_execution_witness.rs
Line: 337-340
Comment:
**Use the existing `NoncontiguousBlockHeaders` error variant**
There is already a typed variant `GuestProgramStateError::NoncontiguousBlockHeaders` (defined a few lines above) with the message "Non-contiguous block headers (there's a gap in the block headers list)". Using `Custom(...)` here bypasses that variant and makes error matching on the call-site harder.
```suggestion
{
return Err(GuestProgramStateError::NoncontiguousBlockHeaders);
}
```
How can I resolve this? If you propose a fix, please make it concise.
🤖 Claude Code ReviewNow I have a full picture of the PR. Here is my review: PR #6541 Review:
|
| Area | Verdict |
|---|---|
| Spec alignment (EELS / geth) | Correct |
| EF test re-enablement | Correct |
| Ascending header order fix | Correct |
| Contiguity check logic | Correct, comment misleading |
| Tolerance (drop malformed entries) | Correct |
| Codes completeness (error on miss) | Correct |
| Comment clarity | One inaccurate comment (Point 1) |
| Error variant consistency | Minor inconsistency (Point 4) |
The logic is sound and the spec alignment looks right. The main ask before merge is fixing the comment in the malformed-header branch — it currently says the next check "fails" when it is actually skipped.
Automated review by Claude (Anthropic) · sonnet · custom prompt
🤖 Codex Code Review
Assumption: I’m reading the EIP-8025 intent here as “tolerate unused malformed entries, but require the usable header chain itself to be contiguous”. Under that interpretation, Item 1 is still a real validation gap. I couldn’t run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
| for bytes in &value.block_headers_bytes { | ||
| let Ok(header) = BlockHeader::decode(bytes.as_ref()) else { | ||
| // Malformed entry is a chain break; the next parent_hash check fails. | ||
| prev_hash = None; |
There was a problem hiding this comment.
Malformed-header recovery skips contiguity check on next header (the inline I posted before — re-anchoring with line+side):
After a malformed header, prev_hash = None skips the contiguity check on the next successful header — if let Some(expected_parent) = prev_hash is None, so the check is skipped, and we proceed with prev_hash reset to the current header's keccak.
Consequence: an attacker can insert a malformed entry at position N, then place a fake header at position N+1 with arbitrary parent_hash. The for-loop accepts it without verification. Since block_headers is then indexed by number, the fake header's state_root (or anything else) is silently used downstream.
The doc comment says "Malformed entry is a chain break; the next parent_hash check fails" — but the actual behavior is that the check is skipped, not failed.
Two possible fixes:
- Hard-fail on any malformed header:
return Err(GuestProgramStateError::Custom("malformed header in witness"))instead ofprev_hash = None; continue. Matches the strict-validation theme of this PR. - Accept segments but require each segment to start at a known-good boundary (e.g., genesis or the parent of the first executed block).
Option 1 is simpler and more defensible. If EELS does intend lenient acceptance, the comment should at least be corrected to reflect that the rest of the chain after a break is unvalidated.
There was a problem hiding this comment.
Done in b531dab — hard-fail + typed NoncontiguousBlockHeaders. Also propagated the previously-discarded Err from get_first_invalid_block_hash.
| } | ||
| // EELS expects ascending order; we walked the chain backward, so reverse. | ||
| // Ref: https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/stateless.py#L171-L191 | ||
| block_headers_bytes.reverse(); |
There was a problem hiding this comment.
Worth confirming: this code walks the chain backward (parent → ... → child) and pushes to block_headers_bytes, then reverses to ascending. The reverse is correct if the loop pushes in strictly child-to-parent order. The hunk shows the same pattern at line 1678 — both call sites do the same walk and reverse. If they ever diverge (say, one of them changes the iteration direction), the reverse becomes wrong silently. Worth extracting the build-headers logic into a single helper to avoid the parallel-implementation drift risk.
Also worth a unit test asserting block_headers_bytes is in ascending order after this loop — would catch any future change to the loop direction.
There was a problem hiding this comment.
Done in 29c80c2 — extracted build_ascending_ancestor_headers_bytes, both sites share it. Test in test/tests/blockchain/witness_tests.rs.
| let initial_state_root = self | ||
| .headers | ||
| .iter() | ||
| .filter_map(|h| BlockHeader::decode(h).ok()) |
There was a problem hiding this comment.
Lenient .ok() on RPC side is intentional per EIP-8025 — the witness can carry extra entries that don't decode as headers. That's a different choice from the guest-side strict parsing at line 327+ (which now contiguity-checks each header).
This split is correct in shape (RPC = lenient input, guest = strict invariant). Worth a short comment explaining the asymmetry — currently a future maintainer reading just one of these blocks would wonder why the other is different.
| } | ||
| self.codes_hashed.get(&code_hash).cloned().ok_or_else(|| { | ||
| GuestProgramStateError::Database(format!( | ||
| "missing bytecode for hash {} in witness", |
There was a problem hiding this comment.
Good change — the old println!("Missing bytecode... defaulting to empty") silently masked real bugs (missing-witness bytecode would lead to a state root mismatch later, but the println would have already scrolled past in a noisy log). Hard-erroring here surfaces the witness-corruption scenario where it should be surfaced.
One thing worth confirming: are there any callers that legitimately tolerated a missing-bytecode return in the old code? The comment said "In client implementations there are differences and it's natural for some clients to access more/less information in some edge cases" — that's a hint the lenient default was masking a known issue. If there's a real EELS test fixture where bytecode is intentionally absent (e.g., for self-destructed accounts), this strict error would now flag it. Worth confirming against the v0.3.3 fixture suite that bumped in #6498.
There was a problem hiding this comment.
Verified — test-stateless passes 8720/0 with the strict-on-missing-bytecode change in place.
…instead of skipping the failed-decode entry and resetting `prev_hash`. The previous lenient path silently bypassed the parent_hash check on the next valid header, so a witness shaped `[valid_A, malformed, fake_B]` was accepted with `fake_B.parent_hash` arbitrary. The malformed-decode branch now propagates the `RLPDecodeError` via `?`, and the contiguity branch returns the typed `GuestProgramStateError::NoncontiguousBlockHeaders` variant in place of `Custom("witness headers are not contiguous")`.
Stop discarding `Err(...)` from `get_first_invalid_block_hash` in `execute_blocks`. The `if let Ok(Some(...))` pattern silently dropped the `NoncontiguousBlockHeaders` error returned when `next_number != number + 1`, so the number-adjacency check at `block_execution_witness.rs:498` only fired when the parent-hash check at line 509 also passed.
Update the `into_execution_witness` comment near the parent state-root lookup to flag that the local `.ok()` filter on headers does not relax the strict chain check downstream.
…:encode(code_hash)` in `get_account_code` and `get_code_metadata`. `H256` implements `LowerHex` (no `0x` prefix), matching `hex::encode`'s output.
…cks_with_fee_configs` and `generate_witness_from_account_updates` into a free function `build_ascending_ancestor_headers_bytes` next to `find_parent_header`. Both call sites built `block_headers_bytes` with the same loop and the same `EELS expects ascending order` reverse — the parallel implementations were a drift risk noted in review. Add an integration test in `test/tests/blockchain/witness_tests.rs` that builds a 3-block chain on an in-memory store, calls `generate_witness_for_blocks(&[block_2, block_3])`, decodes the resulting `block_headers_bytes`, and asserts the ancestor numbers form a strictly ascending sequence.
…gs in #6527 (zkevm v0.3.3 fixtures + amsterdam stateless coverage), #6516 (EIP-8025 compliance), the v11.0.0 bump, and the rest of main since the branch diverged. Conflicts resolved: - `tooling/ef_tests/blockchain/Makefile`: kept main's explanatory comment on `zkevm-vectors`. - `tooling/ef_tests/blockchain/test_runner.rs`: kept main's improved `run_stateless_from_fixture`, which hard-fails on malformed `statelessOutputBytes` and on parse/conversion errors so the `(false, Err(_))` arm cannot mask regressions, and the `Result<bool, String>` signature for `parse_expected_valid_flag`. Dropped the now-duplicate `let expected_valid = ...and_then(parse_expected_valid_flag).unwrap_or(true)` block that git auto-merged from HEAD. Verified `make -C tooling/ef_tests/blockchain test-stateless` returns 8770 passed / 77 failed — failure set identical to the pre-merge baseline.
There was a problem hiding this comment.
Pull request overview
This PR closes remaining gaps in EIP-8025 stateless witness validation/execution so previously skipped validation_* zkevm fixtures can run through ethrex’s stateless path.
Changes:
- Make RPC witness conversion more tolerant of malformed trie node entries (drop undecodable nodes instead of failing conversion).
- Enforce stricter witness correctness: contiguous header chain validation and erroring on missing bytecode/code-metadata.
- Align witness generation to emit ancestor headers in ascending order, plus add a regression test for header ordering.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/tests/blockchain/witness_tests.rs | Adds an integration test asserting generated witness ancestor headers are ordered ascending. |
| test/tests/blockchain/mod.rs | Registers the new witness_tests module. |
| crates/guest-program/src/common/execution.rs | Propagates errors from witness block-hash validation instead of silently ignoring them. |
| crates/common/types/block_execution_witness.rs | Updates witness parsing/validation: tolerate undecodable trie nodes, validate header contiguity, and error on missing bytecode. |
| crates/blockchain/blockchain.rs | Refactors ancestor-header collection into a helper and reverses output to ascending order. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let mut current_header = walk_start_header.clone(); | ||
| while current_header.hash() != first_needed_block_hash { | ||
| let parent_hash = current_header.parent_hash; | ||
| let current_number = current_header.number - 1; |
There was a problem hiding this comment.
Done in 0f78f21 — checked_sub(1) returning WitnessGeneration if walked past genesis.
| assert!( | ||
| numbers.windows(2).all(|w| w[0] < w[1]), | ||
| "ancestor headers must be ascending, got {numbers:?}" |
There was a problem hiding this comment.
Done in f9f0fdc — pinned exact ancestor numbers via assert_eq!.
| // Headers must decode and form a contiguous chain in list order. | ||
| // Ref: https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/stateless.py#L171-L191 | ||
| let mut block_headers: BTreeMap<u64, BlockHeader> = BTreeMap::new(); | ||
| let mut prev_hash: Option<H256> = None; | ||
| for bytes in &value.block_headers_bytes { | ||
| let header = BlockHeader::decode(bytes.as_ref())?; | ||
| if let Some(expected_parent) = prev_hash | ||
| && header.parent_hash != expected_parent | ||
| { | ||
| return Err(GuestProgramStateError::NoncontiguousBlockHeaders); |
There was a problem hiding this comment.
Stale — from_witness now hard-errors on malformed headers (b531dab). PR description updated.
…r_headers_bytes` so the helper returns a typed `WitnessGeneration` error instead of panicking in debug (or wrapping to `u64::MAX` in release) if the chain walk reaches block 0 without matching `first_needed_block_hash`. Production paths can't trigger this — the cached chain walk always terminates within the witness cache window — but the helper is now load-bearing for both call sites and the underflow surfaced as a confusing error string in the format!(...) message.
…_witness_has_ancestor_headers_in_ascending_order` with `assert_eq!(numbers, vec![block_1.header.number, block_2.header.number])`. The original passes vacuously when `block_headers_bytes` has 0 or 1 entries — `windows(2)` yields an empty iterator and `all(...)` returns true — so a regression that drops or shortens the ancestor list would slip through. Pinning the exact expected ancestors fails loudly instead.
…cution_witness` filter comment. Some clients emit the RLP-null marker for absent trie nodes; `Node::decode` errors on it and `.ok()?` drops it. The previous EIP-8025 cleanup removed this context, so a reader had to dig into other implementations to learn why empty entries are expected here.
Motivation
PR #6527 widened stateless coverage to all
for_amsterdamfixtures and skipped 9validation_*tests that exposed gaps in ethrex's witness handling. This PR closes those gaps so the 9 tests pass.Description
Four spec-aligned changes (refs to EELS
execution-specs@projects/zkevmand gethcore/stateless/database.goare inline in the code):into_execution_witnessdrops trie-node entries that fail RLP-decode instead of failing the whole conversion. Bad/extra entries can't be looked up by hash; if execution requires one, the trie walk errors there.from_witnessrejects malformed headers and chains whereheaders[i].parent_hash != keccak(headers[i-1]). Mirrors EELSvalidate_headers.get_account_code/get_code_metadataerror on missing bytecode instead of silently returning empty code.generate_witness_for_blocksnow emits ancestors in ascending order (was descending). Required so ethrex-generated witnesses satisfy the contiguity check above.Cached pre-fix witnesses are descending; they self-heal within the ~120-block cache window, so no read-side sort is added.
How to test
Expected: 8720 passed / 0 failed / 0 ignored, ~155 s.
test-levmunaffected.Checklist
STORE_SCHEMA_VERSION— N/A