| 
 | 1 | +"""  | 
 | 2 | +This module's Blake2s felt encoding and hashing logic is based on StarkWare's  | 
 | 3 | +sequencer implementation:  | 
 | 4 | +https://github.com/starkware-libs/sequencer/blob/b29c0e8c61f7b2340209e256cf87dfe9f2c811aa/crates/blake2s/src/lib.rs  | 
 | 5 | +"""  | 
 | 6 | + | 
 | 7 | +import hashlib  | 
 | 8 | +from typing import List  | 
 | 9 | + | 
 | 10 | +from starknet_py.constants import FIELD_PRIME  | 
 | 11 | + | 
 | 12 | +SMALL_THRESHOLD = 2**63  | 
 | 13 | +BIG_MARKER = 1 << 31  # MSB mask for the first u32 in the 8-limb case  | 
 | 14 | + | 
 | 15 | + | 
 | 16 | +def encode_felts_to_u32s(felts: List[int]) -> List[int]:  | 
 | 17 | +    """  | 
 | 18 | +    Encode each Felt into 32-bit words following Cairo's encoding scheme.  | 
 | 19 | +
  | 
 | 20 | +    Small values (< 2^63) are encoded as 2 words: [high_32_bits, low_32_bits] from the last 8 bytes.  | 
 | 21 | +    Large values (>= 2^63) are encoded as 8 words: the full 32-byte big-endian split,  | 
 | 22 | +    with the MSB of the first word set as a marker (+2^255).  | 
 | 23 | +
  | 
 | 24 | +    :param felts: List of Felt values to encode  | 
 | 25 | +    :return: Flat list of u32 values  | 
 | 26 | +    """  | 
 | 27 | +    unpacked_u32s = []  | 
 | 28 | +    for felt in felts:  | 
 | 29 | +        # Convert felt to 32-byte big-endian representation  | 
 | 30 | +        felt_as_be_bytes = felt.to_bytes(32, byteorder="big")  | 
 | 31 | + | 
 | 32 | +        if felt < SMALL_THRESHOLD:  | 
 | 33 | +            # Small: 2 limbs only, high-32 then low-32 of the last 8 bytes  | 
 | 34 | +            high = int.from_bytes(felt_as_be_bytes[24:28], byteorder="big")  | 
 | 35 | +            low = int.from_bytes(felt_as_be_bytes[28:32], byteorder="big")  | 
 | 36 | +            unpacked_u32s.append(high)  | 
 | 37 | +            unpacked_u32s.append(low)  | 
 | 38 | +        else:  | 
 | 39 | +            # Big: 8 limbs, big-endian order  | 
 | 40 | +            start = len(unpacked_u32s)  | 
 | 41 | +            for i in range(0, 32, 4):  | 
 | 42 | +                limb = int.from_bytes(felt_as_be_bytes[i : i + 4], byteorder="big")  | 
 | 43 | +                unpacked_u32s.append(limb)  | 
 | 44 | +            # Set the MSB of the very first limb as the Cairo hint does with "+ 2**255"  | 
 | 45 | +            unpacked_u32s[start] |= BIG_MARKER  | 
 | 46 | + | 
 | 47 | +    return unpacked_u32s  | 
 | 48 | + | 
 | 49 | + | 
 | 50 | +def pack_256_le_to_felt(hash_bytes: bytes) -> int:  | 
 | 51 | +    """  | 
 | 52 | +    Packs the first 32 bytes (256 bits) of hash_bytes into a Felt (252 bits).  | 
 | 53 | +    Interprets the bytes as a Felt (252 bits)  | 
 | 54 | +
  | 
 | 55 | +    :param hash_bytes: Hash bytes (at least 32 bytes required)  | 
 | 56 | +    :return: Felt value (252-bit field element)  | 
 | 57 | +    """  | 
 | 58 | +    assert len(hash_bytes) >= 32, "need at least 32 bytes to pack"  | 
 | 59 | +    # Interpret the 32-byte buffer as a little-endian integer and convert to Felt  | 
 | 60 | +    return int.from_bytes(hash_bytes[:32], byteorder="little") % FIELD_PRIME  | 
 | 61 | + | 
 | 62 | + | 
 | 63 | +def blake2s_to_felt(data: bytes) -> int:  | 
 | 64 | +    """  | 
 | 65 | +    Compute Blake2s-256 hash over data and return as a Felt.  | 
 | 66 | +
  | 
 | 67 | +    :param data: Input data to hash  | 
 | 68 | +    :return: Blake2s-256 hash as a 252-bit field element  | 
 | 69 | +    """  | 
 | 70 | +    hash_bytes = hashlib.blake2s(data, digest_size=32).digest()  | 
 | 71 | +    return pack_256_le_to_felt(hash_bytes)  | 
 | 72 | + | 
 | 73 | + | 
 | 74 | +def encode_felt252_data_and_calc_blake_hash(felts: List[int]) -> int:  | 
 | 75 | +    """  | 
 | 76 | +    Encodes Felt values using Cairo's encoding scheme and computes Blake2s hash.  | 
 | 77 | +
  | 
 | 78 | +    This function matches Cairo's encode_felt252_to_u32s hint behavior. It encodes  | 
 | 79 | +    each Felt into 32-bit words, serializes them as little-endian bytes, then  | 
 | 80 | +    computes Blake2s-256 hash over the byte stream.  | 
 | 81 | +
  | 
 | 82 | +    :param felts: List of Felt values to encode and hash  | 
 | 83 | +    :return: Blake2s-256 hash as a 252-bit field element  | 
 | 84 | +    """  | 
 | 85 | +    # Unpack each Felt into 2 or 8 u32 limbs  | 
 | 86 | +    u32_words = encode_felts_to_u32s(felts)  | 
 | 87 | + | 
 | 88 | +    # Serialize the u32 limbs into a little-endian byte stream  | 
 | 89 | +    byte_stream = b"".join(word.to_bytes(4, byteorder="little") for word in u32_words)  | 
 | 90 | + | 
 | 91 | +    # Compute Blake2s-256 over the bytes and pack the result into a Felt  | 
 | 92 | +    return blake2s_to_felt(byte_stream)  | 
 | 93 | + | 
 | 94 | + | 
 | 95 | +def blake2s_hash_many(values: List[int]) -> int:  | 
 | 96 | +    """  | 
 | 97 | +    Hash multiple Felt values using Cairo-compatible Blake2s encoding.  | 
 | 98 | +
  | 
 | 99 | +    This is the recommended way to hash Felt values for Starknet when using  | 
 | 100 | +    Blake2s as the hash method.  | 
 | 101 | +
  | 
 | 102 | +    :param values: List of Felt values to hash  | 
 | 103 | +    :return: Blake2s-256 hash as a 252-bit field element  | 
 | 104 | +    """  | 
 | 105 | +    return encode_felt252_data_and_calc_blake_hash(values)  | 
0 commit comments