fix(types): make the arbitrary feature self-contained - #231
Conversation
`cargo check -p arc-consensus-types --features arbitrary` fails:
error[E0433]: failed to resolve: could not find `Arbitrary` in `arbitrary`
--> crates/types/src/address.rs:39
| #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
The feature gates `derive(arbitrary::Arbitrary)` on `Address`, but it
declared neither of the two things that derive needs:
- the `derive` feature of the `arbitrary` crate, which provides the
derive macro itself;
- `alloy-primitives/arbitrary`, without which the generated impl fails
with `the trait bound alloy_primitives::Address: Arbitrary<'_> is not
satisfied`, since the wrapped type has no `Arbitrary` impl.
This is invisible in a workspace build: `arc-consensus-db` and
`arc-node-consensus` both depend on `alloy-rpc-types-engine` with
`features = ["arbitrary"]`, and feature unification turns on what this
crate omitted. Building the crate on its own is what exposes it.
Declare both, following the pattern already used by `arc-evm` and
`arc-node`, whose `arbitrary` features forward to their alloy
dependencies explicitly.
Cargo.lock is unchanged. `cargo check --workspace --all-features` still
passes, and `cargo test -p arc-consensus-types --features arbitrary`
passes (192 tests).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
osr21
left a comment
There was a problem hiding this comment.
Verified everything independently against main before reviewing:
- The broken state is as described:
arbitrary = ["dep:arbitrary"]with noderiveand noalloy-primitives/arbitraryforwarding, whileaddress.rsgatesderive(arbitrary::Arbitrary)on the feature. Both missing pieces are real and both are needed — fixing only one moves the error fromE0433to theE0277bound failure, exactly as the issue documents. - The
arc-evmprecedent holds: itsarbitraryfeature forwards to the alloy/revm/reth deps explicitly, so this change follows existing workspace convention rather than inventing one. - The masking analysis is the most valuable part of the PR and it's correct, with one detail worth making explicit: this only stays hidden because workspace-level commands (
--workspace --all-features) unify features across all members. With resolver v2,cargo check -p <crate>resolves only that crate's own feature graph — which is why the standalone check is both the honest reproduction and a valid verification that the fix is complete (no unification bailing it out).
Two notes, neither blocking:
1. Placement of derive is equivalent to forwarding, with one subtle property worth knowing. Putting features = ["derive"] on the optional dependency declaration (rather than "arbitrary/derive" in the feature array) works because features on an optional dep only activate when the dep does. The one behavioral difference: if a second feature ever enables dep:arbitrary for a different purpose, it inherits derive whether it wants it or not, whereas the feature-array form keeps the choice per-feature. For a proc-macro feature on a fuzzing dep, that's a non-concern in practice — just noting it's a deliberate trade, and the current form is the more readable of the two.
2. This bug class is systematic, and there's a cheap guard. Feature-unification masking will re-occur for any crate whose optional features are exercised in CI only via workspace-wide builds — this crate just happens to be the first one caught building standalone. cargo hack check --each-feature -p <crate> (or workspace-wide cargo hack check --each-feature --workspace, which is the standard tool for exactly this) would catch the whole class in CI. Worth a follow-up issue rather than this PR, but without it the next #[cfg_attr(feature = ...)] derive added to any crate can silently regress the same way.
The restraint on the workspace-level arbitrary = "1.3" is also correct — pushing derive into the workspace declaration would force the proc-macro dependency on every consumer to fix one crate's declaration gap.
Minimal, correct, well-evidenced. Approving.
Fixes #233.
Summary
The
arbitraryfeature ofarc-consensus-typesdoes not build on its own:The feature gates
derive(arbitrary::Arbitrary)onAddress, but declares neither of the two things that derive needs:arbitrary/derive— the derive macro itself lives behind that feature, hence theE0433above.alloy-primitives/arbitrary— without it the generated impl fails withthe trait bound alloy_primitives::Address: Arbitrary<'_> is not satisfied, since the wrapped type has noArbitraryimpl of its own.Why CI does not catch this
arc-consensus-dbandarc-node-consensusboth depend onalloy-rpc-types-enginewithfeatures = ["arbitrary"]. In a workspace build, feature unification turns on exactly what this crate omitted, so--all-featuresat the workspace level compiles and the gap stays hidden. It only surfaces when the crate is built alone — which is also how a consumer would build it, andcrates/typesinheritspublishfrom the workspace rather than opting out.Change
Declare both, following the pattern
arc-evmandarc-nodealready use, where thearbitraryfeature forwards to the alloy dependencies explicitly:Testing
cargo check -p arc-consensus-types --features arbitrary— now compiles (this is the command that fails onmain).cargo check --workspace --all-features— still passes, no regression.cargo test -p arc-consensus-types --features arbitrary— 192 tests pass.cargo fmt --all --check— clean.Cargo.lockis unchanged:arbitrary'sderivefeature andalloy-primitives/arbitraryare both already enabled somewhere in the graph, so no version resolution moves.I left the workspace-level
arbitrary = "1.3"in the rootCargo.tomlalone deliberately — addingderivethere would enable it for every consumer of the workspace dependency, and only this crate needs it.