Skip to content

refactor(felt): phantom-typed Typed[K] as the single felt implementation - #4205

Merged
EgeCaner merged 4 commits into
mainfrom
refactor/felt/typed-kinds
Oct 26, 2026
Merged

EgeCaner merged 4 commits into
mainfrom
refactor/felt/typed-kinds

Conversation

@EgeCaner

@EgeCaner EgeCaner commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

User description

What

core/felt now has one field element type, Typed[K any] fp.Element, told apart at compile time by a phantom kind. Felt = Typed[feltKind]; Address, Hash, ClassHash, SierraClassHash, CasmClassHash, TransactionHash and StateRootHash are aliases over unexported kinds. hash.go, address.go and unsafe.go are deleted. Ten files, all in core/felt; nothing outside the package changes and all short tests pass.

Rationale

We want dedicated felt types everywhere, and as they spread they must carry the full method set, especially the methods libraries call indirectly: MarshalCBOR/UnmarshalCBOR and the text/JSON hooks. A type declared as type Hash felt.Felt inherits none of them, and the CBOR codec does not complain: it silently falls back to reflection over the four-word array. Today that yields the same bytes as Felt's fast path by coincidence of layout, which is how Hash, Address and friends have been written to disk so far. The planned on-disk encoding breaks that coincidence: a derived type without its own MarshalCBOR would keep writing the legacy limb array and fail, or be misread, on the new format.

With Typed[K] every alias inherits every method including the codec hooks, adding a kind is one struct and one alias line, mixing kinds is a compile error, and converting between kinds is a free cast. typed_test.go pins each alias to Felt's CBOR, text and raw bytes across zero, small, single-word and full-width values, so the equivalence is tested rather than accidental. The retype lands before the codec change so every felt type goes through one codec entry point before that entry point changes format.

Design choices

  • Generic methods are one-liners over concrete code. Go emits a per-kind symbol for each instantiated method; if the generic body is large that symbol becomes a trampoline into the shared shape body with a dictionary. Measured as about 4% on JSON unmarshal of []felt.Felt. So each Typed[K] method converts to fp.Element and calls a concrete function (setHex, setString, appendHex, unmarshalCBOR, …); the per-kind symbol inlines it and calls concrete code directly. Verified in the assembler output: no dictionary, no dynamic dispatch, identical allocations.
  • Slice[F FeltLike] keeps the spelling felt.Slice[felt.Felt]. A generic type cannot infer its argument, so a kind-parameterised slice would need a public kind name at 118 call sites, and a self-referential constraint would keep the spelling at the cost of a dictionary call per element. Both rejected; the decoder parses into an fp.Element and relabels with F(element), which replaces the old unsafe pointer cast at no cost.
  • IsZero/Equal keep FeltLike signatures and delegate to gnark through a value conversion that compiles to loads through the pointer, no copy. Every felt type now also has them as methods, so the twelve production call sites can move to the method during call-site migration.
  • %T prints felt.Typed[felt.hashKind] instead of felt.Hash; compiler errors still use the alias names.

Benchmarks versus origin/main

Both test binaries run alternately six times, benchstat over twelve samples (sequential runs drift up to 5% here).

benchmark main this PR
MarshalCBOR / UnmarshalCBOR, non-zero 5.42 / 4.45 ns 5.41 / 4.65 ns
JSONUnmarshal, random felt 159.1 ns 159.6 ns
MarshalText, full width 34.17 ns 34.52 ns
JSONMarshal, random felt 161.0 ns 164.3 ns
JSON unmarshal felt.Slice, 1000 / 5000 37.34 / 185.3 µs 37.41 / 188.8 µs
JSON marshal felt.Slice, 1000 25.06 µs 25.47 µs
JSON unmarshal []felt.Felt, 1000 56.27 µs 57.25 µs
JSON marshal []felt.Felt, 1000 33.66 µs 36.14 µs (+7%)
B/op, allocs/op, every row identical identical

The one visible cost is JSON marshal of a plain []felt.Felt: json/v2 reaches AppendText through the type's method table, which on main pointed at the body and now points at the per-kind symbol that makes one direct call to appendHex, 2–3 ns per felt. It is a static call, inherent to methods on an instantiated generic type, and felt.Slice, the type hot lists should use anyway, does not pay it.


PR Type

Enhancement


Description

  • Replace dedicated felt types with a phantom-typed Typed[K] to share a single implementation.

  • Delete boilerplate alias files (address.go, hash.go) and remove the use of unsafe.go.

  • Update IsZero and Equal utils to compare array limbs directly instead of value conversions.

  • Ensure all derived felt types inherit CBOR and JSON/text codec hooks automatically.


File Walkthrough

Relevant files
Miscellaneous
3 files
address.go
Delete redundant Address type and methods                               
+0/-41   
hash.go
Delete redundant Hash aliases and methods                               
+0/-211 
unsafe.go
Delete unsafe pointer casting utility                                       
+0/-9     
Enhancement
5 files
cbor.go
Update CBOR codec to use Typed[K] and fp.Element                 
+11/-6   
felt.go
Introduce Typed[K] and migrate Felt methods to it               
+75/-49 
slice.go
Use fp.Element for appending hex strings and unmarshaling
+5/-8     
typed.go
Define phantom kinds and aliases for felt types                   
+22/-0   
utils.go
Refactor IsZero and Equal to compare limbs directly           
+4/-2     
Documentation
1 files
ctors.go
Update FeltLike interface comments                                             
+2/-2     
Tests
3 files
ctors_test.go
Refactor test type to use Typed kind                                         
+4/-1     
typed_test.go
Add tests to ensure Typed formats identically to Felt       
+68/-0   
utils_test.go
Add test for helper functions matching fp.Element behavior
+20/-0   

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @EgeCaner's task in 6m 39s —— View job


Review: core/felt phantom-typed Typed[K] refactor

I reviewed the full diff (git diff origin/main...HEAD) against the stated goal — collapsing Felt/Hash/Address/etc. into one generic implementation so every alias inherits the full method set (especially MarshalCBOR/UnmarshalCBOR and the text/JSON hooks).

Summary: clean. No correctness bugs, panics, or regressions found.

What I checked:

  • core/felt/felt.go, typed.go: the Typed[K] fp.Element retype and per-kind methods are mechanical renames of the old Felt methods (z *Felt → z *Typed[K]), with free functions (setHex, setString, setRandom, shortString, appendHex) factored out so the generic method bodies stay one-liners, matching the PR's inlining rationale. Logic is byte-for-byte equivalent to the old code paths.
  • cbor.go: MarshalCBOR/UnmarshalCBOR now generic over Typed[K], delegating to the same encodeLimbs/decodeLimbs/decodeFelt fast-path helpers (unchanged, and covered by the pre-existing, untouched cbor_fastpath_test.go fuzz/stress tests).
  • slice.go: the UnmarshalJSONFrom fast path replaces the old asFeltPtr(&newFelt).setHex(...) unsafe-pointer cast with setHex(&element, ...) into a plain fp.Element, then relabels via F(element) — same layout guarantee (FeltLike is ~[4]uint64), no unsafe needed anymore, and this is exactly what the PR description claims.
  • utils.go (IsZero/Equal, second commit): switched from fp.Element value-conversion + method call to direct *v == F{} / *a == *b limb comparison. Typed[K]'s underlying array layout makes == the same predicate as fp.Element.Equal/IsZero (both just compare the 4 limbs), and TestHelpersMatchElement in utils_test.go pins this against fp.Element's own methods across zero/one/random values. Correct.
  • typed_test.go: TestTypedEncodesLikeFelt round-trips CBOR, text, String(), Marshal(), and Bytes() for every alias (Address, Hash, ClassHash, SierraClassHash, CasmClassHash, TransactionHash, StateRootHash) against Felt, across zero/small/single-word/full-width values — good coverage for the "every alias must encode like Felt" invariant this PR exists to guarantee.
  • Deleted address.go, hash.go, unsafe.go: confirmed no remaining references to asFeltPtr or the old per-type method bodies anywhere in the tree.
  • Searched all external usages of felt.Hash, felt.Address, felt.ClassHash, etc. (migration/, core/state/, core/trie2/, blockchain/, rpc/v8..v10, consensus/starknet/) — all are direct conversions (felt.Address(*owner)) or type aliases, none depend on removed exported symbols, and nothing outside core/felt needed changes, matching the PR's claim.
  • One pre-existing pattern worth flagging for context (not introduced by this PR, not a blocker): consensus/starknet/starknet.go:10 still has type Value felt.Hash — a derived type that does not inherit the full Typed method set (same issue the PR's rationale describes for Hash/Address before this refactor). It's out of scope here since the PR explicitly only touches core/felt, but it's a candidate for the same treatment once the codec format changes land, since it's exactly the kind of type the PR warns about.

No blockers, no important issues, no nits beyond the note above (informational only, not a flag on this PR's diff).

@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.73%. Comparing base (765b182) to head (3c075ad).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4205      +/-   ##
==========================================
- Coverage   80.85%   80.73%   -0.12%     
==========================================
  Files         512      509       -3     
  Lines       39124    39062      -62     
==========================================
- Hits        31634    31537      -97     
- Misses       7481     7516      +35     
  Partials        9        9              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

  Delegating to fp.Element through a value conversion compiled to two 32-byte
  stack copies per call; the array comparison reads the limbs through the
  pointers and is the same predicate. A test pins both helpers to the field
  element's own IsZero and Equal.
@EgeCaner
EgeCaner force-pushed the refactor/felt/typed-kinds branch from c810171 to b6103a2 Compare October 26, 2026 12:58
@EgeCaner
EgeCaner requested a review from infrmtcs October 26, 2026 12:58
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @EgeCaner's task in 3m 25s —— View job


Review: core/felt phantom-typed Typed[K] refactor

  • Gather context (PR description, prior automated review, changed files)
  • Read all changed files in core/felt (felt.go, typed.go, cbor.go, slice.go, utils.go, ctors.go + tests) and diff each against origin/main's deleted address.go/hash.go/unsafe.go
  • Check correctness, concurrency, error handling, perf, security, tests
  • Post summary

Clean. This is a mechanical, well-tested retype with no behavioral changes — every diffed method body is byte-for-byte equivalent to its pre-refactor counterpart, just generalized from *Felt/*Hash/*Address receivers to *Typed[K], with the free-function extraction (setHex, setString, setRandom, shortString, appendHex) matching the stated "generic methods are one-liners" design goal. Specifics I checked:

  • felt.go / typed.go: Typed[K] fp.Element + phantom kind aliases — all methods present on old Felt (SetBytesCanonical, Equal, IsZero, arithmetic, Clone, etc.) carry over unchanged in logic.
  • cbor.go: MarshalCBOR/UnmarshalCBOR now generic over Typed[K], delegating to the same unchanged encodeLimbs/decodeLimbs/decodeFelt fast-path helpers (still covered by the pre-existing cbor_fastpath_test.go).
  • slice.go: the unsafe-pointer cast (asFeltPtr) is gone; UnmarshalJSONFrom now decodes into a plain fp.Element and relabels with F(element) — same layout guarantee (FeltLike is ~[4]uint64), correct and simpler.
  • utils.go: IsZero/Equal switched from fp.Element method calls to direct limb-array equality (==). Since fp.Element's own IsZero/Equal are themselves limb comparisons against the (Montgomery-form) representation, this is equivalent — and it's pinned by the new TestHelpersMatchElement in utils_test.go against fp.Element's own methods across zero/one/random values.
  • typed_test.go: TestTypedEncodesLikeFelt round-trips CBOR, text, String(), Marshal(), Bytes() for every alias against Felt across zero/small/single-word/full-width values — solid coverage of the exact invariant ("every alias must encode like Felt") this PR exists to guarantee.
  • Deleted address.go, hash.go, unsafe.go: confirmed no remaining references to asFeltPtr or the old per-type method bodies.
  • Confirmed the diff is scoped exactly to core/felt (12 files, matches the PR's stat) and no external package needed changes — felt.Hash/felt.Address/etc. usages elsewhere are direct conversions or pass-through, unaffected by the retype.

No blockers, no important issues, no nits. (Restating the one informational note from the earlier automated pass: consensus/starknet/starknet.go has type Value felt.Hash, a derived type that doesn't inherit Typed's method set — out of scope for this PR since it only touches core/felt, but a candidate for the same treatment later.)

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Title

(Describe updated until commit 3c075ad)

refactor(felt): phantom-typed Typed[K] as the single felt implementation


PR Type

Enhancement


Description

  • Unify all felt types as phantom-typed Typed[K] aliases

  • Delete per-type forwarding files hash.go and address.go

  • Genericize CBOR and JSON codec hooks over Typed[K]

  • Compare limbs directly in IsZero/Equal; add tests


File Walkthrough

Relevant files
Enhancement
8 files
typed.go
Add phantom kinds and Typed[K] type aliases                           
+25/-0   
felt.go
Convert Felt to generic Typed[K] with helpers                       
+50/-44 
hash.go
Delete hash-type forwarding methods now aliased                   
+0/-211 
address.go
Delete Address forwarding methods now aliased                       
+0/-41   
unsafe.go
Delete unsafe asFeltPtr pointer reinterpretation helper   
+0/-9     
cbor.go
Genericize CBOR codec hooks over Typed[K]                               
+5/-5     
slice.go
Use shared appendHex/setHex in Slice codec                             
+5/-8     
utils.go
Compare limbs directly in IsZero and Equal                             
+4/-2     
Documentation
1 files
ctors.go
Document FeltLike constraint for generic code                       
+2/-2     
Tests
3 files
utils_test.go
Pin helpers to fp.Element's own comparisons                           
+20/-0   
typed_test.go
Pin all alias encodings to Felt's bytes                                   
+68/-0   
ctors_test.go
Test external kind gets full Typed API                                     
+4/-1     

Comment thread core/felt/felt.go
Comment thread core/felt/cbor.go Outdated
Comment thread core/felt/felt.go Outdated

@infrmtcs infrmtcs left a comment

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.

The rest LGTM, thanks!

@EgeCaner
EgeCaner deployed to Development October 26, 2026 13:22 — with GitHub Actions Active
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @EgeCaner's task in 6m 17s —— View job


Review: core/felt phantom-typed Typed[K] refactor

  • Gather context (diff vs origin/main, prior automated reviews, human review thread)
  • Read all changed files in core/felt (felt.go, typed.go, cbor.go, slice.go, utils.go, ctors.go + tests)
  • Check correctness, concurrency, error handling, perf, security, tests
  • Post summary

Clean — no blockers, no important issues, no new nits.

This is a mechanical retype (*Felt/*Hash/*Address receivers → *Typed[K]) with no behavioral changes. I diffed every method body against its pre-refactor counterpart and confirmed logical equivalence:

  • felt.go / typed.go: Typed[K] fp.Element + phantom kind aliases. Free-function extraction (setHex, setString, setRandom, shortString, appendHex) matches the PR's stated inlining rationale — generic method bodies stay one-liners that delegate to concrete, non-generic code.
  • cbor.go: MarshalCBOR/UnmarshalCBOR generic over Typed[K], delegating to the unchanged encodeLimbs/decodeLimbs/decodeFelt fast-path helpers (still covered by the pre-existing cbor_fastpath_test.go).
  • slice.go: the old asFeltPtr unsafe-pointer cast is gone; UnmarshalJSONFrom decodes into a plain fp.Element and relabels via F(element) — same layout guarantee (FeltLike is ~[4]uint64), simpler and no longer unsafe.
  • utils.go: IsZero/Equal switched to direct limb-array equality (==), pinned against fp.Element's own IsZero/Equal by the new TestHelpersMatchElement across zero/one/random values.
  • typed_test.go: TestTypedEncodesLikeFelt round-trips CBOR, text, String(), Marshal(), Bytes() for every alias against Felt, across zero/small/single-word/full-width values — solid coverage of the exact invariant this PR exists to guarantee.
  • Deleted address.go, hash.go, unsafe.go: confirmed no remaining references to asFeltPtr anywhere in the tree.
  • Confirmed no file outside core/felt references Typed[K] directly, and all external felt.Address/felt.Hash/etc. usages (rpc/v8-v10, core/trie2, blockchain, migration, consensus) are plain conversions/aliases unaffected by the retype — matches the PR's "nothing outside the package changes" claim.
  • The latest commit (843e7da) already addresses the one human review thread (grouping Felt's alias with the others in typed.go, documenting the inlining rule). The other two review comments (keeping Typed exported; keeping unmarshalCBOR as a separate helper for inlining) were answered by the author with concrete rationale and left as-is — both are reasonable engineering tradeoffs, not correctness issues.

No action needed from me.

@EgeCaner
EgeCaner deployed to Development October 26, 2026 14:25 — with GitHub Actions Active
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @EgeCaner's task in 3m 13s —— View job


Review: core/felt phantom-typed Typed[K] refactor

  • Gather context (PR description, prior automated reviews, human review thread)
  • Diff latest commit (3c075ad96) vs the previously-reviewed state, plus full diff vs origin/main
  • Check correctness, concurrency, error handling, perf, security, tests
  • Post summary

Clean — no blockers, no important issues, no nits.

This update adds one commit (3c075ad96) on top of what was already reviewed: it inlines setString, setRandom, and shortString back into their Typed[K] methods in felt.go, and inlines the (*fp.Element)(z) cast directly into UnmarshalCBOR in cbor.go, dropping the separate unmarshalCBOR helper. This is exactly the resolution the author described in the review thread on cbor.go — after measuring with -gcflags=-m=2/-S, the helper split turned out to be perf-neutral (both forms compile to a non-inlinable call either way), so the helper was removed for simplicity. I diffed each inlined method against its prior form and confirmed the logic is byte-for-byte identical, just without the extra indirection — no behavioral change:

  • SetString: same big-int parse/bounds-check/SetBytesCanonical sequence, now returning (z, err) directly instead of delegating.
  • SetRandom: same SetRandom+panic-on-error sequence.
  • ShortString: same hex-truncation logic.
  • UnmarshalCBOR: same decodeFelt fast path → cbor.Unmarshal fallback, cast inlined at each call site.
  • Confirmed no leftover references to the removed unmarshalCBOR/setString/setRandom/shortString helpers anywhere in the tree.

Combined with the rest of the diff (already reviewed across three prior passes and approved by a human reviewer): the Typed[K] fp.Element retype, phantom-kind aliases in typed.go, the IsZero/Equal limb-equality optimization (pinned by TestHelpersMatchElement against fp.Element's own methods), and the CBOR/JSON encoding-equivalence tests in typed_test.go all remain correct and unchanged in behavior. Nothing outside core/felt is affected.
(branch)

@EgeCaner
EgeCaner deployed to Development October 26, 2026 15:07 — with GitHub Actions Active
@EgeCaner
EgeCaner merged commit 301cbb9 into main Oct 26, 2026
27 of 28 checks passed
@EgeCaner
EgeCaner deleted the refactor/felt/typed-kinds branch October 26, 2026 15:32

This branch was successfully deployed

1 active deployment
Development — 3c075ad9 Deployed Oct 26, 2026 by EgeCaner via dev_deploy_test / deploy #6182
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants