Skip to content

test: load the frame transaction fixtures by mapping their fork name - #12854

Draft
Marchhill wants to merge 2 commits into
eip8141-frame-txs-devnet7from
claude/eip8141-fixture-suite
Draft

test: load the frame transaction fixtures by mapping their fork name#12854
Marchhill wants to merge 2 commits into
eip8141-frame-txs-devnet7from
claude/eip8141-fixture-suite

Conversation

@Marchhill

@Marchhill Marchhill commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Changes

  • Map Bogota in SpecNameParser, so the EIP-8141 fixture suites — which declare that network — load instead of failing every file.
  • Narrow the HalfBlockchainTestJson shape fallback in ConvertToBlockchainTests to deserialization only. It previously wrapped conversion too, so an unmapped fork name was reported as an unrelated Hash256 conversion error against the trimmed shape rather than as itself.
  • Name the offending fork in the NotSupportedException instead of throwing it bare.

Before this change the whole suite failed to load; after it, all 159 blockchain cases load and execute.

Fixtures come from the tests-frames-devnet@v0.0.0 release (fixtures_frames-devnet.tar.gz). Note that EIP-8141 merged to the eips/amsterdam/eip-8141 branch rather than master, and the pyspec DEFAULT_ARCHIVE_VERSION pin contains none of these tests, so wiring this into CI needs a second archive pin.

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Description

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

SpecNameParserTests covers both the mapping and the exception message. Both assertions were revert-checked: removing the mapping and restoring the bare throw fails them.

Documentation

Requires documentation update

  • Yes
  • No

Requires explanation in Release Notes

  • Yes
  • No

Remarks

The state-test path does not gate anything — do not wire it into CI

TransactionJson has no frames/signatures fields, so a frame transaction cannot be represented in a state-test fixture. The 40 EIP-8141 state-test files therefore load to zero cases, and the runner exits 0. Anything that runs that path today reports a pass while asserting nothing. Only the blockchain-test path, which decodes block RLP and so exercises the real frame decoder, is meaningful. Supporting state tests means adding the frame fields to the loader; until then that path should be treated as unwired, not as green.

Loading is only half the story

This branch composes Bogota on Osaka, whereas the fixtures compose it on Amsterdam, so 96 of the 159 cases are still rejected at BlockLevelAccessListHashNotEnabled before they execute. That fork-composition question is deliberately left open here.

Frame targets under EIP-7702 delegation

With Bogota composed on Amsterdam for measurement, the delegated-target cases fail on every branch tried, including one carrying devnet-8 gas and the frame entry charge. ExecuteFrame resolves the target's code with

CodeInfo codeInfo = _codeInfoRepository.GetCachedCodeInfo(resolvedTarget, spec, out _);

which follows the delegation for code but discards the delegation address, so the delegation target's access cost is never charged. Affects test_delegated_target_entry_charge (cold and warm), test_delegated_to_precompile_target and test_verify_frame_delegated_to_precompile_target.

The EIP-8141 fixture suites declare their network as Bogota, which
SpecNameParser did not map, so every file failed to load.

The failure surfaced as an unrelated Hash256 conversion error because
ConvertToBlockchainTests wrapped both deserialization and conversion in
the HalfBlockchainTestJson shape fallback: the unmapped fork name threw
during conversion, and the retry against the trimmed shape then failed
on its differently typed postState. Narrow the fallback to
deserialization and name the fork in the exception.
@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 5m 19s —— View job


Review: PR #12854

  • Gather context (diff, changed files, base branch)
  • Review SpecNameParser change
  • Review ConvertToBlockchainTests fallback narrowing
  • Review new test file / project wiring
  • Post findings (3 inline comments)

Verdict

The core change is right, and the diagnosis in the commit message checks out. I traced the masking path: HalfBlockchainTestJson.PostState is Hash256 shadowing the base Dictionary<Address, AccountState>?, so a standard-shaped fixture deserializes fine, LoadSpecSpecNameParser.Parse throws bare on Bogota during conversion, and the old catch-all then retried the whole thing against the trimmed shape, where postState as a dict blows up on Hash256. The narrowing is the correct fix.

I also checked the narrowing doesn't lose a load-bearing path. For the fallback to have been useful across conversion, the standard deserialize would have to succeed and the half-shape deserialize succeed on the same JSON — mutually exclusive, since a dict postState fails Hash256 and a scalar postState fails the dict. Convert (line 351) already discriminates via as HalfBlockchainTestJson, so CoerceFromHalf preserving the runtime type still works. Both overloads are non-iterator and eagerly return a List, so exceptions still surface inside FileTestsSource's per-file try and become a FailedToLoadTest rather than aborting a directory. No regression found.

One Medium, on test strength rather than production behaviour. Nothing Critical or High. No security or performance surface — all three files are test-path or a cold parser behind a ConcurrentDictionary cache.

Sev Finding
Medium Parse_maps_Bogota_to_the_frame_transactions_fork asserts IsEip8141Enabled, which Eip8141Prototype also satisfies — the mapping the test names is not actually pinned (inline)
Low catch (Exception) still discards the standard-shape error, so a fixture matching neither shape is still reported only against the trimmed type (inline)
Low Exception message interpolates specName but the switch matches unambiguousSpecName (inline)
Low Rationale comment should be <remarks> per AGENTS.md, applies to both overloads but sits on one, and is phrased as change history (inline)
Low Assert.Throws<T> returns TActual? under <Nullable>enable</Nullable> → CS8600; sibling test uses ! (inline)
Low No drift test — the recurring failure mode is the switch diverging from Nethermind.Specs.Forks, and this project already has the reflection pattern for it in ChainSpecHardforkLabelTests.ForkFor (inline)

Two things about scope, for the reviewer

Neither is a defect — both follow from decisions the PR body already states — but they change what "loads" means here.

No fixture class is registered. Ethereum.Blockchain.Pyspec.Test/Tests.cs has no BogotaBlockchainTests, and Constants.DEFAULT_ARCHIVE_NAME is still fixtures_glamsterdam-devnet.tar.gz. So nothing in CI exercises the new switch arm beyond the unit test, and the 96 BlockLevelAccessListHashNotEnabled rejections in the Remarks can't turn CI red. Given Bogota() : NamedReleaseSpec<Bogota>(Osaka.Instance) versus the fixtures' Amsterdam composition, holding the registration back is the right call — worth stating explicitly so a later reader doesn't assume the suite is wired.

The description contradicts itself. "all 159 blockchain cases load and execute" sits against Remarks' "96 of the 159 cases are still rejected … before they execute." The Remarks version is the accurate one; the summary line reads as a stronger claim than the branch supports.

What I could not verify

Build and test execution were blocked — dotnet build on Nethermind.Specs.Test requires approval in this environment, so both findings about that project are static reads. I'm confident on the CS8600 (NUnit 4's Throws<TActual> returns TActual?; the csproj sets <Nullable>enable</Nullable> with no TreatWarningsAsErrors, so it warns rather than breaks the build) and on name resolution (SpecNameParser resolves through the enclosing Nethermind.Specs namespace without an explicit using). If you want the suite actually run, re-trigger with dotnet in --allowedTools.

Blocking on the Medium only in the sense that the new test doesn't yet guard what its name promises — a one-line change to Is.SameAs(Bogota.Instance).
· branch claude/eip8141-fixture-suite

Comment on lines +420 to +426
// The shape fallback deliberately wraps deserialization only: letting it span conversion
// made an unmapped fork name resurface as an unrelated error against the trimmed shape.
public static IEnumerable<BlockchainTest> ConvertToBlockchainTests(string json)
{
try { return ConvertToBlockchainTests(_serializer.Deserialize<Dictionary<string, BlockchainTestJson>>(json)); }
catch (Exception) { return ConvertToBlockchainTests(CoerceFromHalf(_serializer.Deserialize<Dictionary<string, HalfBlockchainTestJson>>(json))); }
Dictionary<string, BlockchainTestJson> tests;
try { tests = _serializer.Deserialize<Dictionary<string, BlockchainTestJson>>(json); }
catch (Exception) { tests = CoerceFromHalf(_serializer.Deserialize<Dictionary<string, HalfBlockchainTestJson>>(json)); }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — the other half of the same masking problem is still there.

The narrowing is correct and is the right fix — but catch (Exception) still discards the standard-shape exception entirely. When a fixture is neither shape (malformed JSON, a genuinely new field, a type change on the standard side), the only error that reaches FileTestsSource's Failed to load: {e} is the trimmed-shape error, which is exactly the "reported as an unrelated error against the trimmed shape" confusion this PR set out to remove. It just moved from conversion errors to deserialization errors.

Chaining the original preserves both:

Dictionary<string, BlockchainTestJson> tests;
try { tests = _serializer.Deserialize<Dictionary<string, BlockchainTestJson>>(json); }
catch (Exception standardShape)
{
    try { tests = CoerceFromHalf(_serializer.Deserialize<Dictionary<string, HalfBlockchainTestJson>>(json)); }
    catch (Exception halfShape) { throw new AggregateException("Fixture matched neither the standard nor the trimmed shape", standardShape, halfShape); }
}

Also relevant: robustness.md — "Never swallow exceptions … at minimum log the exception." catch (Exception) here doesn't even bind the exception. Pre-existing, but these are the lines the diff touches.

Two secondary notes on the same hunk:

  • Per AGENTS.md member-doc rules, a rationale that "applies to the member as a whole" should be <remarks>, not a leading in-line comment. As written it also sits above the string overload only, while describing both.
  • The comment is phrased historically ("letting it span conversion made an unmapped fork name resurface"), which reads as a changelog entry rather than an invariant. Something like <remarks>Scoped to deserialization so that conversion errors — e.g. an unmapped fork name — are reported as themselves rather than as a shape mismatch against the trimmed type.</remarks> survives better on the base branch.

Fix this →

"Amsterdam" => Amsterdam.Instance,
_ => throw new NotSupportedException()
"Bogota" => Bogota.Instance,
_ => throw new NotSupportedException($"Unknown fork name '{specName}'")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — the message names the wrong string.

The switch matches on unambiguousSpecName, but the message interpolates specName. For any input that goes through the substitution chain above the two differ, and the name reported is not the one that failed to match:

  • "Merge+9999" → matched as "Paris+9999", reported as `Merge+9999`
  • "Shanghai+3541" → matched as "Shanghai+3541", reported the same (no substitution) — fine
  • "GrayGlacier+2929" → matched as "GrayGlacier+2929" — fine

So it only misleads for the Merge/Merged/EIP150/EIP158/DAO families, but those are the cases where a reader would most want to know what the parser actually looked up. Including both costs nothing:

Suggested change
_ => throw new NotSupportedException($"Unknown fork name '{specName}'")
_ => throw new NotSupportedException($"Unknown fork name '{specName}' (resolved to '{unambiguousSpecName}')")

Fix this →

Comment on lines +12 to +17
[Test]
public void Parse_maps_Bogota_to_the_frame_transactions_fork()
{
IReleaseSpec spec = SpecNameParser.Parse("Bogota");

Assert.That(spec.IsEip8141Enabled, Is.True);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — this assertion doesn't pin the mapping the test name claims.

IsEip8141Enabled is not unique to Bogota. Nethermind.Specs/Forks/Eip8141Prototype.cs also exists in this tree, and any future 8141-bearing fork would satisfy this too. Map "Bogota" to the wrong 8141 spec and the test stays green, so the "maps Bogota to the frame transactions fork" contract is untested — only "maps Bogota to something with 8141 on".

Assert identity instead; Parse returns the singleton, so reference equality is the exact contract:

Suggested change
[Test]
public void Parse_maps_Bogota_to_the_frame_transactions_fork()
{
IReleaseSpec spec = SpecNameParser.Parse("Bogota");
Assert.That(spec.IsEip8141Enabled, Is.True);
[Test]
public void Parse_maps_Bogota_to_the_frame_transactions_fork()
{
IReleaseSpec spec = SpecNameParser.Parse("Bogota");
Assert.That(spec, Is.SameAs(Bogota.Instance));
}

(needs using Nethermind.Specs.Forks;)

Low, same file, line 23: Assert.Throws<T> returns TActual?, and this project has <Nullable>enable</Nullable>, so NotSupportedException e = Assert.Throws<...>(...) emits CS8600. ChainSpecHardforkLabelTests.cs:172 in the same project uses the ! suffix — worth matching.

Low, structural: the recurring failure here isn't "Bogota specifically was missing", it's "SpecNameParser's switch drifts from Nethermind.Specs.Forks". This project already has the tool for that — ChainSpecHardforkLabelTests.ForkFor reflects over Nethermind.Specs.Forks.{name}.Instance precisely to avoid a hand-maintained mapping. A [TestCaseSource] over the NamedReleaseSpec types in that namespace asserting each is parseable would catch the next one at compile-of-the-fork time. It needs an explicit skip set — Olympic and MuirGlacier are fork classes that SpecNameParser deliberately doesn't map — and that set being explicit is itself the documentation. Optional for this PR, but it's the difference between fixing one instance and closing the class.

Fix this →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant