Skip to content

feat: implement compression for ContractStorageAccount values - #594

Open
S0naliThakur wants to merge 1 commit into
SHARD-2702from
value-compression
Open

S0naliThakur wants to merge 1 commit into
SHARD-2702from
value-compression

Conversation

@S0naliThakur

@S0naliThakur S0naliThakur commented Sep 11, 2025 •

Copy link
Copy Markdown
Member

User description

Adds functionality to compress and decompress storage values in the ContractStorageAccount, improving space efficiency by storing values as compressed binary instead of hex strings. feat: enhance handling of ContractStorageAccount values

Implements logic to compress and decompress ContractStorageAccount values, ensuring efficient storage management. Additionally, updates transaction state handling to account for empty storage slots, improving robustness in RLP decoding. This change optimizes data retrieval and storage efficiency across the application. refactor: simplify account data handling by removing compression logic

refactor: streamline RLP decoding logic in transaction state management


PR Type

Enhancement, Tests


Description

  • Implements compression for ContractStorageAccount storage values.

  • Updates serialization/deserialization to use compressed binary format.

  • Adds robust conversion between hex strings and Uint8Array.

  • Expands and adapts unit tests for new compression logic.


Changes walkthrough 📝

Relevant files
Enhancement
ContractStorageAccount.ts
Add compressed binary storage and enhanced serialization for
ContractStorageAccount

src/types/ContractStorageAccount.ts

  • Adds compression and decompression functions for storage values.
  • Updates serialization to store values as compressed binary with length
    prefix.
  • Updates deserialization to handle compressed binary and legacy
    formats.
  • Refactors conversion functions to use new compression logic.
  • Improves error handling and conversion from hex strings.
  • +100/-20
    contractStorageCompression.ts
    Add contract storage value compression/decompression utilities

    src/storage/contractStorageCompression.ts

  • Introduces utility functions for compressing/decompressing contract
    storage values.
  • Handles Uint8Array and JSON serialization edge cases.
  • Ensures robust restoration of storage values from various formats.
  • +50/-0   
    accountStorage.ts
    Simplify account query result structure                                   

    src/storage/accountStorage.ts

  • Refactors account query functions to simplify result structure.
  • Removes redundant wrapping of account data in returned results.
  • +2/-8     
    Tests
    ContractStorageAccount.test.ts
    Update and expand tests for compressed ContractStorageAccount values

    test/unit/src/types/ContractStorageAccount.test.ts

  • Updates and expands tests for new compression and serialization logic.
  • Adds tests for edge cases, large values, and binary handling.
  • Verifies compatibility and correctness of conversion functions.
  • Ensures decompression and padding logic is thoroughly tested.
  • +51/-22 

    Need help?
  • Type /help how to ... in the comments thread for any questions about PR-Agent usage.
  • Check out the documentation for more information.
  • Adds functionality to compress and decompress storage values in the ContractStorageAccount, improving space efficiency by storing values as compressed binary instead of hex strings.
    feat: enhance handling of ContractStorageAccount values
    
    Implements logic to compress and decompress ContractStorageAccount values, ensuring efficient storage management. Additionally, updates transaction state handling to account for empty storage slots, improving robustness in RLP decoding. This change optimizes data retrieval and storage efficiency across the application.
    refactor: simplify account data handling by removing compression logic
    
    refactor: streamline RLP decoding logic in transaction state management
    @github-actions

    Copy link
    Copy Markdown
    Contributor

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
    🏅 Score: 92
    🧪 PR contains tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Serialization/Deserialization Consistency

    The new compression and decompression logic for storage values in ContractStorageAccount introduces a custom binary format. Ensure that all edge cases (e.g., all-zero values, values longer than 32 bytes, odd-length hex strings) are consistently handled during both serialization and deserialization, and that backward compatibility is maintained where required.

    function compressValue(bytes: Uint8Array): Uint8Array {
      if (bytes.length === 0) return new Uint8Array([0])
    
      // Find first non-zero byte
      let firstNonZero = 0
      while (firstNonZero < bytes.length && bytes[firstNonZero] === 0) {
        firstNonZero++
      }
    
      // All zeros case - store as single zero byte
      if (firstNonZero === bytes.length) return new Uint8Array([0])
    
      // Return only the significant bytes (no length prefix needed for serialization)
      return bytes.slice(firstNonZero)
    }
    
    /**
     * Decompress value - if compressed is larger than 32 bytes, return as-is
     * Otherwise pad with leading zeros to 32 bytes
     */
    function decompressValue(compressed: Uint8Array): Uint8Array {
      if (compressed.length === 1 && compressed[0] === 0) {
        return new Uint8Array(32) // All zeros, pad to 32 bytes
      }
    
      // If compressed value is larger than 32 bytes, return as-is (no truncation)
      if (compressed.length > 32) {
        return compressed
      }
    
      // Pad smaller values to 32 bytes with leading zeros
      const result = new Uint8Array(32)
      result.set(compressed, 32 - compressed.length)
    
      return result
    }
    
    
    /**
     * Convert minimal hex string to Uint8Array
     */
    function fromMinimalHex(hex: string): Uint8Array {
      if (!hex.startsWith('0x')) {
        throw new Error('Hex string must start with 0x')
      }
    
      const cleanHex = hex.slice(2)
      if (cleanHex.length === 0 || cleanHex === '0') {
        return new Uint8Array([0])
      }
    
      // Pad odd length hex strings
      const paddedHex = cleanHex.length % 2 === 0 ? cleanHex : '0' + cleanHex
    
      const bytes = new Uint8Array(paddedHex.length / 2)
      for (let i = 0; i < paddedHex.length; i += 2) {
        bytes[i / 2] = parseInt(paddedHex.substring(i, i + 2), 16)
      }
    
      return bytes
    }
    
    /**
     * Optimized ContractStorage account with compressed value storage.
     * Value is stored as compressed binary instead of hex string for better space efficiency.
     */
    export interface ContractStorageAccount extends BaseAccount {
      accountType: AccountType.ContractStorage
      hash: string         // Account hash (required by Shardus)
      timestamp: number    // Account timestamp (required by Shardus)
      key: string          // Storage key (essential)
      value: Uint8Array    // Storage value as compressed binary (removes leading zeros)
    }
    
    /**
     * Serialize ContractStorageAccount with compressed value.
     * Value is stored as length-prefixed compressed binary.
     */
    export function serializeContractStorageAccount(
      stream: VectorBufferStream,
      obj: ContractStorageAccount,
      root = false
    ): void {
      if (root) {
        stream.writeUInt16(TypeIdentifierEnum.cContractStorageAccount)
      }
      stream.writeUInt8(cContractStorageAccountVersion)
    
      // Serialize base account (accountType)
      serializeBaseAccount(stream, obj, false)
    
      // Serialize essential ContractStorage fields only
      stream.writeString(obj.hash)
      stream.writeBigUInt64(BigInt(obj.timestamp))
      stream.writeString(obj.key)
    
      // Compress and serialize value with length prefix
      const compressedValue = compressValue(obj.value)
      stream.writeUInt8(compressedValue.length)
      if (compressedValue.length > 0) {
        stream.writeBuffer(Buffer.from(compressedValue))
      }
    }
    
    /**
     * Deserialize ContractStorageAccount with compressed value support.
     * Handles both v1 (hex string) and v2 (compressed binary) formats.
     */
    export function deserializeContractStorageAccount(stream: VectorBufferStream): ContractStorageAccount {
      const version = stream.readUInt8()
      if (version > cContractStorageAccountVersion) {
        throw new Error('ContractStorageAccount version mismatch')
      }
    
      const baseAccount = deserializeBaseAccount(stream)
      const hash = stream.readString()
      const timestamp = Number(stream.readBigUInt64())
      const key = stream.readString()
    
      // Read compressed binary value
      const compressedLength = stream.readUInt8()
      let compressedValue: Uint8Array
      if (compressedLength > 0) {
        const buffer = stream.readBuffer()
        compressedValue = new Uint8Array(buffer)
      } else {
        compressedValue = new Uint8Array([0])
      }
      const value = decompressValue(compressedValue)
    API Return Structure Change

    The return value of queryAccountsEntryByRanges and queryAccountsEntryByRanges2 has changed from an object containing accountData and accountId to just the account data. This may break consumers expecting the previous structure. Review all usages to ensure compatibility.

          processedResults.push(result.data)
        }
        return processedResults
      } else {
        throw Error('not supported here')
      }
    }
    
    export async function queryAccountsEntryByRanges2(
      accountStart,
      accountEnd,
      tsStart,
      tsEnd,
      maxRecords,
      offset,
      accountOffset
    ): Promise<Array<{ accountData: WrappedEVMAccount; accountId: string }>> {
      if (ShardeumFlags.UseDBForAccounts === true) {
        const processedResults = []
        let results
    
        if (accountOffset != null && accountOffset.length > 0) {
          results = await storage.queryAccountsEntryByRanges3(
            accountStart,
            accountEnd,
            tsStart,
            tsEnd,
            maxRecords,
            accountOffset
          )
        } else {
          results = await storage.queryAccountsEntryByRanges2(accountStart, accountEnd, tsStart, tsEnd, maxRecords, offset)
        }
    
        for (const result of results) {
          if (typeof result.data === 'string') {
            result.data = Utils.safeJsonParse(result.data)
          }
          processedResults.push(result.data)
        }

    Comment on lines +133 to +141
    const compressedLength = stream.readUInt8()
    let compressedValue: Uint8Array
    if (compressedLength > 0) {
    const buffer = stream.readBuffer()
    compressedValue = new Uint8Array(buffer)
    } else {
    compressedValue = new Uint8Array([0])
    }
    const value = decompressValue(compressedValue)

    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.

    Suggestion: The current code does not verify that the length of the buffer read matches the expected compressedLength, which could allow malformed or truncated data to be accepted. Add a check to ensure the buffer length matches compressedLength and throw an error if it does not. [possible issue, importance: 7]

    Suggested change
    const compressedLength = stream.readUInt8()
    let compressedValue: Uint8Array
    if (compressedLength > 0) {
    const buffer = stream.readBuffer()
    compressedValue = new Uint8Array(buffer)
    } else {
    compressedValue = new Uint8Array([0])
    }
    const value = decompressValue(compressedValue)
    const compressedLength = stream.readUInt8()
    let compressedValue: Uint8Array
    if (compressedLength > 0) {
    const buffer = stream.readBuffer()
    if (buffer.length !== compressedLength) {
    throw new Error('Compressed value length mismatch')
    }
    compressedValue = new Uint8Array(buffer)
    } else {
    compressedValue = new Uint8Array([0])
    }
    const value = decompressValue(compressedValue)

    Comment on lines +54 to +73
    function fromMinimalHex(hex: string): Uint8Array {
    if (!hex.startsWith('0x')) {
    throw new Error('Hex string must start with 0x')
    }

    const cleanHex = hex.slice(2)
    if (cleanHex.length === 0 || cleanHex === '0') {
    return new Uint8Array([0])
    }

    // Pad odd length hex strings
    const paddedHex = cleanHex.length % 2 === 0 ? cleanHex : '0' + cleanHex

    const bytes = new Uint8Array(paddedHex.length / 2)
    for (let i = 0; i < paddedHex.length; i += 2) {
    bytes[i / 2] = parseInt(paddedHex.substring(i, i + 2), 16)
    }

    return bytes
    }

    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.

    Suggestion: The function does not check for invalid hex characters, which could result in NaN values in the output array. Add a check after parsing each byte to ensure it is a valid number, and throw an error if not. [possible issue, importance: 8]

    Suggested change
    function fromMinimalHex(hex: string): Uint8Array {
    if (!hex.startsWith('0x')) {
    throw new Error('Hex string must start with 0x')
    }
    const cleanHex = hex.slice(2)
    if (cleanHex.length === 0 || cleanHex === '0') {
    return new Uint8Array([0])
    }
    // Pad odd length hex strings
    const paddedHex = cleanHex.length % 2 === 0 ? cleanHex : '0' + cleanHex
    const bytes = new Uint8Array(paddedHex.length / 2)
    for (let i = 0; i < paddedHex.length; i += 2) {
    bytes[i / 2] = parseInt(paddedHex.substring(i, i + 2), 16)
    }
    return bytes
    }
    function fromMinimalHex(hex: string): Uint8Array {
    if (!hex.startsWith('0x')) {
    throw new Error('Hex string must start with 0x')
    }
    const cleanHex = hex.slice(2)
    if (cleanHex.length === 0 || cleanHex === '0') {
    return new Uint8Array([0])
    }
    // Pad odd length hex strings
    const paddedHex = cleanHex.length % 2 === 0 ? cleanHex : '0' + cleanHex
    const bytes = new Uint8Array(paddedHex.length / 2)
    for (let i = 0; i < paddedHex.length; i += 2) {
    const byte = parseInt(paddedHex.substring(i, i + 2), 16)
    if (isNaN(byte)) {
    throw new Error('Invalid hex string')
    }
    bytes[i / 2] = byte
    }
    return bytes
    }

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    1 participant