Skip to content

Stabilize ChainConfig serde for Human-Readable & Binary Formats #2436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

Wollac
Copy link
Contributor

@Wollac Wollac commented May 13, 2025

Motivation

This PR refactors Genesis serde logic to fix roundtrip errors and ensure a clear distinction between human-readable (JSON) and binary formats.

Fixes #2435

Solution

This PR implements the following changes:

  • Updated deserialize_private_key, deserialize_storage_map, and introduced the ttd module. All now use is_human_readable() to ensure symmetric (de)serialization:
    • TTD is now serialized as a raw JSON number (using u128) in human-readable contexts and via standard U256 serde for binary formats.
    • The human-readable deserialisation was not changed.
  • deserialize_json_ttd_opt is kept for backward compatibility.
  • Other optional numeric fields in ChainConfig now also use is_human_readable() to ensure symmetric (de)serialization:
    • The human-readable deserialisation still supports number or a "quantity" hex string.
    • For non-human-readable formats the default is used for both serialization and deserialization.

With this PR it is now possible to (de)serialize Genesis using binary formats (such as serde_cbor). For JSON, the deserialization behavior remains completely unchanged to preserve backward compatibility, only for serialization it fixes the issue that the terminal_total_difficulty was not serialized as a number.

PR Checklist

  • Added Tests
  • Added Documentation
  • Breaking changes

Copy link
Member

Choose a reason for hiding this comment

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

removing hex support will break existing setups, so we cant remove this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only for JSON files that contain hex, which neither alloy nor geth ever produce. Do you still think we should support hex deserialization in human-readable input?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Arguably, using alloy-serde::opt also for serialization, would be an even more breaking change.
So the only alternative would be to add a alloy-genesis specific deserializer, which uses default when non-human-readable and alloy_serde::quantity::opt::deserialize when human-readable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 823a16b

Copy link
Member

Choose a reason for hiding this comment

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

this should remain unchanged to ensure this change is not breaking

Copy link
Contributor Author

@Wollac Wollac May 13, 2025

Choose a reason for hiding this comment

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

I'm not sure I really understand why this would break, but I've reverted this change: d6521f1

@github-project-automation github-project-automation bot moved this to In Progress in Alloy May 13, 2025
Some(value) => {
// convert into an u128 when possible
let number = value.try_into().map_err(ser::Error::custom)?;
serializer.serialize_u128(number)
Copy link
Member

Choose a reason for hiding this comment

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

this does not work for json, the entire reason for this workaround is that this does not work for mainnet TTD because does not fit in json number without enabling arb precision, whcih we shoudl not do

Copy link
Contributor Author

@Wollac Wollac May 13, 2025

Choose a reason for hiding this comment

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

It does work with serde_json, and I've also added a tests for that.
Serde has (limited) support for u128, even when the arbitrary precision feature is not enabled. However, there is no support for deserializing u128 inside an untagged enum: serde-rs/serde#2230
And as I understand the comments, the workaround has been implemented to support exactly untagged enums. (I even added a test for this particular case).

If we don't need to consider untagged enums (which are irrelevant for ChainConfig), we could even get rid of the workaround altogether and use u128.

@Wollac Wollac requested a review from mattsse May 14, 2025 14:36
@Wollac
Copy link
Contributor Author

Wollac commented May 14, 2025

@mattsse I changed it so that the deserialization is now 100% identical to and compatible with the existing version.

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

I'm a bit confused why we need to change this via the deserde impls

because this still keeps only deserialize_with = "deserialize_u64_opt"

which doesn't seem symmetrical to me

wouldn't this still deserialize hex and serialize u64?

Comment on lines 296 to 308
if deserializer.is_human_readable() {
match Option::<String>::deserialize(deserializer)? {
Some(ref s) => {
if s == "0x" {
return Ok(None);
}
B256::from_str(s).map(Some).map_err(D::Error::custom)
}
None => Ok(None),
}
B256::from_str(s).map(Some).map_err(D::Error::custom)
} else {
Ok(None)
Option::<B256>::deserialize(deserializer)
}
Copy link
Member

Choose a reason for hiding this comment

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

hmm, why we need to change this here, is this because the non human readable impl for b256 is different?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In binary, a B256 will not be serialized as a string, but as 32 bytes. Thus in a roundtrip, the Option::deserialize would fail. It only works for human-readable where it is serialized as a hex string.

@mattsse
Copy link
Member

mattsse commented May 14, 2025

oh I think I get it now

@Wollac
Copy link
Contributor Author

Wollac commented May 15, 2025

I think it would conceptually be cleaner (symmetry and Geth compatibility) to only support parsing JSON numbers and not also hex strings.
However, technically this would be a breaking change for config files in a strange format that used to work before. Thus, I've proposed to serialize to JSON numbers, and support the deserialization of both.

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

okay, I think this makes sense, I only have one last question about removed test cases and one nit

@Wollac Wollac requested a review from mattsse May 22, 2025 14:00
@mattsse
Copy link
Member

mattsse commented May 22, 2025

cool, I think this is good now
I'm delaying merging this a bit because we have fusaka genesis changes wip and it'd be easier to rebase this pr

@mattsse
Copy link
Member

mattsse commented Jun 4, 2025

hey @Wollac sorry for the delay, we just merged #2542

mind rebasing this pr and update the new fields as well? then we can include this one


/// Custom deserialization function for `Option<u64>`.
///
/// This function allows it to be deserialized form a number or a "quantity" hex string.
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a small typo in the docstring: form a number should be from a number.

Suggested change
/// This function allows it to be deserialized form a number or a "quantity" hex string.
/// This function allows it to be deserialized from a number or a "quantity" hex string.

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

[Bug] serde for ChainConfig: Roundtrip failures & missing format distinction
2 participants