Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ More expansive patch notes and explanations may be found in the specific [pathfi
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Changed

- BREAKING: JSON-RPC input no longer accepts hex strings that are missing the `0x` prefix, in line with the Starknet API spec.

## [0.22.2] - 2026-04-07

### Changed
Expand Down
2 changes: 1 addition & 1 deletion crates/class-hash/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ pub mod json {
// fields (which we now need to ignore if empty).

let expected = ComputedClassHash::Cairo(class_hash!(
"056b96c1d1bbfa01af44b465763d1b71150fa00c6c9d54c3947f57e979ff68c3"
"0x056b96c1d1bbfa01af44b465763d1b71150fa00c6c9d54c3947f57e979ff68c3"
));

// Known contract which triggered a hash mismatch failure.
Expand Down
6 changes: 3 additions & 3 deletions crates/common/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ use crate::macro_prelude::block_hash;
use crate::BlockHash;

pub const MAINNET_GENESIS_HASH: BlockHash =
block_hash!("047C3637B57C2B079B93C61539950C17E868A28F46CDEF28F88521067F21E943");
block_hash!("0x047C3637B57C2B079B93C61539950C17E868A28F46CDEF28F88521067F21E943");

pub const SEPOLIA_TESTNET_GENESIS_HASH: BlockHash =
block_hash!("5c627d4aeb51280058bed93c7889bce78114d63baad1be0f0aeb32496d5f19c");
block_hash!("0x5c627d4aeb51280058bed93c7889bce78114d63baad1be0f0aeb32496d5f19c");

pub const SEPOLIA_INTEGRATION_GENESIS_HASH: BlockHash =
block_hash!("19f675d3fb226821493a6ab9a1955e384bba80f130de625621a418e9a7c0ca3");
block_hash!("0x19f675d3fb226821493a6ab9a1955e384bba80f130de625621a418e9a7c0ca3");
12 changes: 6 additions & 6 deletions crates/common/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ fn starkhash_to_dec_str(h: &Felt) -> String {

/// A helper conversion function. Only use with __sequencer API related types__.
fn starkhash_from_dec_str(s: &str) -> Result<Felt, anyhow::Error> {
match BigUint::from_str(s) {
Ok(b) => {
let h = Felt::from_be_slice(&b.to_bytes_be())?;
Ok(h)
}
// The order here matters because `Felt::from_hex_str` requires the '0x'
// prefix, so we'll never parse a hex string as a decimal string by mistake.
match Felt::from_hex_str(s) {
Ok(h) => Ok(h),
Err(_) => {
let h = Felt::from_hex_str(s)?;
let b = BigUint::from_str(s)?;
let h = Felt::from_be_slice(&b.to_bytes_be())?;
Ok(h)
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/common/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ macro_rules! felt {
Err(pathfinder_crypto::HexParseError::InvalidLength { .. }) => {
panic!("Too many hex digits")
}
Err(pathfinder_crypto::HexParseError::MissingPrefix) => {
panic!("Missing '0x' prefix")
}
Err(pathfinder_crypto::HexParseError::Overflow) => panic!("Felt overflow"),
};
CONST_FELT
Expand Down
94 changes: 47 additions & 47 deletions crates/crypto/src/algebra/field/felt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,8 @@ impl From<CurveOrderMontFelt> for Felt {
impl Felt {
/// A convenience function which parses a hex string into a [Felt].
///
/// Supports both upper and lower case hex strings, as well as an optional
/// "0x" prefix.
/// Supports both upper and lower case hex strings. The '0x' prefix is
/// mandatory.
pub const fn from_hex_str(hex_str: &str) -> Result<Self, HexParseError> {
const fn parse_hex_digit(digit: u8) -> Result<u8, HexParseError> {
match digit {
Expand All @@ -336,19 +336,16 @@ impl Felt {
}

let bytes = hex_str.as_bytes();
let start = if bytes.len() >= 2 && bytes[0] == b'0' && bytes[1] == b'x' {
2
} else {
0
let len = match bytes {
[b'0', b'x', rest @ ..] if rest.len() > 64 => {
return Err(HexParseError::InvalidLength {
max: 64,
actual: rest.len(),
})
}
[b'0', b'x', rest @ ..] => rest.len(),
_ => return Err(HexParseError::MissingPrefix),
};
let len = bytes.len() - start;

if len > 64 {
return Err(HexParseError::InvalidLength {
max: 64,
actual: bytes.len(),
});
}

let mut buf = [0u8; 32];

Expand All @@ -358,7 +355,7 @@ impl Felt {
// Handle a possible odd nibble remaining nibble.
if len % 2 == 1 {
let idx = len / 2;
buf[31 - idx] = match parse_hex_digit(bytes[start]) {
buf[31 - idx] = match parse_hex_digit(bytes[2]) {
Ok(b) => b,
Err(e) => return Err(e),
};
Expand Down Expand Up @@ -461,6 +458,7 @@ impl Felt {
pub enum HexParseError {
InvalidNibble(u8),
InvalidLength { max: usize, actual: usize },
MissingPrefix,
Overflow,
}

Expand All @@ -479,6 +477,7 @@ impl std::fmt::Display for HexParseError {
Self::InvalidLength { max, actual } => {
f.write_fmt(format_args!("More than {} digits found: {}", *max, *actual))
}
Self::MissingPrefix => f.write_str("Missing '0x' prefix"),
Self::Overflow => f.write_str(OVERFLOW_MSG),
}
}
Expand All @@ -498,7 +497,7 @@ mod tests {

#[test]
fn view_bits() {
let one = Felt::from_hex_str("1").unwrap();
let one = Felt::from_hex_str("0x1").unwrap();

let one = one.view_bits().to_bitvec();

Expand Down Expand Up @@ -566,7 +565,7 @@ mod tests {

#[test]
fn round_trip() {
let original = Felt::from_hex_str("abcdef0123456789").unwrap();
let original = Felt::from_hex_str("0xabcdef0123456789").unwrap();
let bytes = original.to_be_bytes();
let result = Felt::from_be_slice(&bytes[..]).unwrap();

Expand All @@ -575,15 +574,15 @@ mod tests {

#[test]
fn too_long() {
let original = Felt::from_hex_str("abcdef0123456789").unwrap();
let original = Felt::from_hex_str("0xabcdef0123456789").unwrap();
let mut bytes = original.to_be_bytes().to_vec();
bytes.push(0);
Felt::from_be_slice(&bytes[..]).unwrap_err();
}

#[test]
fn short_slice() {
let original = Felt::from_hex_str("abcdef0123456789").unwrap();
let original = Felt::from_hex_str("0xabcdef0123456789").unwrap();
let bytes = original.to_be_bytes();
let result = Felt::from_be_slice(&bytes[24..]);

Expand All @@ -610,50 +609,50 @@ mod tests {

#[test]
fn debug() {
let hex_str = "1234567890abcdef000edcba0987654321";
let hex_str = "0x1234567890abcdef000edcba0987654321";
let felt = Felt::from_hex_str(hex_str).unwrap();
let result = format!("{felt:?}");

let mut expected = "0".repeat(64 - hex_str.len());
expected.push_str(hex_str);
let expected = format!("Felt({felt})");
let leading_zeros = "0".repeat(64 + 2 - hex_str.len());
let hex_upper_stripped = hex_str.strip_prefix("0x").unwrap().to_uppercase();
let expected = format!("Felt(0x{leading_zeros}{hex_upper_stripped})");

assert_eq!(result, expected);
}

#[test]
fn fmt() {
let hex_str = "1234567890abcdef000edcba0987654321";
let hex_str = "0x1234567890abcdef000edcba0987654321";
let starkhash = Felt::from_hex_str(hex_str).unwrap();
let result = format!("{starkhash:x}");

let mut expected = "0".repeat(64 - hex_str.len());
expected.push_str(hex_str);
let mut expected = "0".repeat(64 - hex_str.len() + 2);
expected.push_str(&hex_str[2..]);

// We don't really care which casing is used by fmt.
assert_eq!(result.to_lowercase(), expected.to_lowercase());
}

#[test]
fn lower_hex() {
let hex_str = "1234567890abcdef000edcba0987654321";
let hex_str = "0x1234567890abcdef000edcba0987654321";
let starkhash = Felt::from_hex_str(hex_str).unwrap();
let result = format!("{starkhash:x}");

let mut expected = "0".repeat(64 - hex_str.len());
expected.push_str(hex_str);
let mut expected = "0".repeat(64 - hex_str.len() + 2);
expected.push_str(&hex_str[2..]);

assert_eq!(result, expected.to_lowercase());
}

#[test]
fn upper_hex() {
let hex_str = "1234567890abcdef000edcba0987654321";
let hex_str = "0x1234567890abcdef000edcba0987654321";
let starkhash = Felt::from_hex_str(hex_str).unwrap();
let result = format!("{starkhash:X}");

let mut expected = "0".repeat(64 - hex_str.len());
expected.push_str(hex_str);
let mut expected = "0".repeat(64 - hex_str.len() + 2);
expected.push_str(&hex_str[2..]);

assert_eq!(result, expected.to_uppercase());
}
Expand Down Expand Up @@ -686,27 +685,13 @@ mod tests {

#[test]
fn simple() {
let (test_str, expected) = test_data();
let uut = Felt::from_hex_str(test_str).unwrap();
assert_eq!(uut, expected);
}

#[test]
fn prefix() {
let (test_str, expected) = test_data();
let uut = Felt::from_hex_str(&format!("0x{test_str}")).unwrap();
assert_eq!(uut, expected);
}

#[test]
fn leading_zeros() {
let (test_str, expected) = test_data();
let uut = Felt::from_hex_str(&format!("000000000{test_str}")).unwrap();
assert_eq!(uut, expected);
}

#[test]
fn prefix_and_leading_zeros() {
let (test_str, expected) = test_data();
let uut = Felt::from_hex_str(&format!("0x000000000{test_str}")).unwrap();
assert_eq!(uut, expected);
Expand All @@ -717,9 +702,24 @@ mod tests {
assert_matches!(Felt::from_hex_str("0x123z").unwrap_err(), HexParseError::InvalidNibble(n) => assert_eq!(n, b'z'))
}

#[test]
fn missing_prefix() {
assert_matches!(
Felt::from_hex_str("123").unwrap_err(),
HexParseError::MissingPrefix
)
}

#[test]
fn invalid_len() {
assert_matches!(Felt::from_hex_str(&"1".repeat(65)).unwrap_err(), HexParseError::InvalidLength{max: 64, actual: n} => assert_eq!(n, 65))
let invalid_str = "0".repeat(65);
assert_matches!(
Felt::from_hex_str(&format!("0x{invalid_str}")).unwrap_err(),
HexParseError::InvalidLength {
max: 64,
actual: 65
}
);
}

#[test]
Expand Down
8 changes: 6 additions & 2 deletions crates/crypto/src/algebra/field/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl<'de> Deserialize<'de> for Felt {
type Value = Felt;

fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a hex string of up to 64 digits with an optional '0x' prefix")
formatter.write_str("a hex string of up to 64 digits with a mandatory '0x' prefix")
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
Expand All @@ -53,7 +53,11 @@ mod tests {

#[test]
fn empty() {
assert_eq!(serde_json::from_str::<Felt>(r#""""#).unwrap(), Felt::ZERO);
assert!(serde_json::from_str::<Felt>(r#""""#)
.unwrap_err()
.to_string()
// Serde errors also include the line/column.
.starts_with("Missing '0x' prefix"));
assert_eq!(serde_json::from_str::<Felt>(r#""0x""#).unwrap(), Felt::ZERO);
}

Expand Down
21 changes: 12 additions & 9 deletions crates/crypto/src/hash/pedersen/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,18 @@ mod tests {
#[test]
fn test_pedersen() {
// Test vector from https://github.com/starkware-libs/crypto-cpp/blob/master/src/starkware/crypto/pedersen_hash_test.cc
let a =
Felt::from_hex_str("03d937c035c878245caf64531a5756109c53068da139362728feb561405371cb")
.unwrap();
let b =
Felt::from_hex_str("0208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a")
.unwrap();
let expected =
Felt::from_hex_str("030e480bed5fe53fa909cc0f8c4d99b8f9f2c016be4c41e13a4848797979c662")
.unwrap();
let a = Felt::from_hex_str(
"0x03d937c035c878245caf64531a5756109c53068da139362728feb561405371cb",
)
.unwrap();
let b = Felt::from_hex_str(
"0x0208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a",
)
.unwrap();
let expected = Felt::from_hex_str(
"0x030e480bed5fe53fa909cc0f8c4d99b8f9f2c016be4c41e13a4848797979c662",
)
.unwrap();

let hash = pedersen_hash(a, b);
assert_eq!(hash, expected);
Expand Down
6 changes: 4 additions & 2 deletions crates/crypto/src/hash/poseidon/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,10 @@ mod tests {
#[test]
fn test_sponge() {
let expected_result = MontFelt::from(
Felt::from_hex_str("07b8f30ac298ea12d170c0873f1fa631a18c00756c6e7d1fd273b9a239d0d413")
.unwrap(),
Felt::from_hex_str(
"0x07b8f30ac298ea12d170c0873f1fa631a18c00756c6e7d1fd273b9a239d0d413",
)
.unwrap(),
);

// Construct messages, the first few integers
Expand Down
Loading
Loading