Vyper Fixed Size Byte Arrays (Usage and Examples)

whiteboard crypto logo
Published by:
Whiteboard Crypto
on

In Vyper, an M-byte-wide fixed-size byte array (notated as bytesM) is a crucial data structure, designed specifically to store byte-like data.  Here are some examples:

hash: bytes32

@external
def store_hash(_hash: bytes32):
    self.hash = _hash 

@external
def retrieve_hash() -> bytes32:
    return self.hash


hash: bytes32

@external
def compute_keccak256(_input: bytes32):
    self.hash = keccak256(_input)

@external
def concatenate_two_hashes(_hash1: bytes32, _hash2: bytes32):
    self.hash = concat(_hash1, _hash2)

@external
def get_first_half(_hash: bytes32) -> bytes16:
    return slice(_hash, start=0, len=16)


ethereum_address: bytes20

@external
def store_address(_address: bytes20):
    # Store an Ethereum address
    self.ethereum_address = _address 

@external
def retrieve_address() -> bytes20:
    # Retrieve the stored address
    return self.ethereum_address

Unlike their dynamic counterparts, dynamic byte arrays, this data type is unique in accommodating an exact amount of data, restricted to M bytes. This article aims to delve into this data type’s intricacies, its practical implications within Vyper, and wraps up with some best practices for developers to observe.

Overview of M-byte-wide Fixed Size Byte Arrays

The M-byte-wide fixed-size byte array, represented as bytesM in Vyper, is a versatile data type that plays an essential role in handling byte-like data of a fixed size in smart contracts. The ‘M’ in bytesM signifies the length of the byte array – it denotes how many bytes the array can hold and can range from 1 to 32.

Compared to dynamic byte arrays, which can flexibly grow or shrink in size, fixed-size byte arrays have a set size. This distinction makes fixed size byte arrays incredibly beneficial when working with data that you know will always conform to a certain length. For instance, when you’re handling Ethereum addresses (which are always 20 bytes long), you would use bytes20 to store each address in your contract.

This ability to specify the length of byte arrays brings about more efficient memory usage. By stipulating the data length, you avoid allocating excessive memory for variable-sized data, which can lead to significant gas savings when deploying and interacting with your Vyper contracts.

In addition to efficient memory usage, fixed size byte arrays enable the use of certain built-in functions such as keccak256concat, and slice which provide the handling convenience on these byte arrays. This combination of memory efficiency and functional convenience makes the ‘M-byte-wide Fixed Size Byte Array’ a powerful tool in a Vyper developer’s toolkit.

Common Uses of Fixed Size Byte Arrays

Knowing how to judiciously use bytesM can significantly improve your smart contract’s efficiency, both in terms of storage and gas consumption. Here are a common situations where you’re likely to use bytesM:

  1. Storing Ethereum Addresses: As mentioned earlier, Ethereum addresses are always 20 bytes in length. By using bytes20, you can optimally store these addresses in your contract.
  2. Fingerprinting Data: Fixed-size byte arrays are often used to store keccak or SHA hashes for data fingerprinting. If you’re storing the keccak-256 hash of a piece of data, you would use bytes32.
  3. Storing Fixed-Length Identifiers: If your contract uses fixed-size identifiers (like a 16-byte UUID), a fixed-size byte array would be the perfect fit.
  4. Interacting with External Contracts: When your contract interacts with an external contract, the function signatures are typically represented as a 4-byte identifier. Here, you would use bytes4.

Example 1: Storing and Retrieving a Hash

In this first example, we demonstrate how to store a keccak-256 hash (a bytes32 fixed-size byte array) within a smart contract and how to retrieve this hash. This utilization is commonly encountered in contracts that deal with data verification and validation.

hash: bytes32

@external
def store_hash(_hash: bytes32):
    # Store the hash value
    self.hash = _hash 

@external
def retrieve_hash() -> bytes32:
    # Retrieve the stored hash
    return self.hash

In this simple example, a smart contract declares a variable hash of type bytes32. The function store_hash allows us to store a hash, and the retrieve_hash function retrieves the stored hash.

Example 2: Utilizing In-built Operators

Vyper comes equipped with multiple operators that come in handy while dealing with fixed-size byte arrays:

  • keccak256(x): Returns the keccak256 hash of the byte array x.
  • concat(x, ...): Concatenates multiple byte arrays.
  • slice(x, start=_start, len=_len): Returns a slice of _len starting at _start.
hash: bytes32

@external
def compute_keccak256(_input: bytes32):
    # Calculate and store the keccak256 hash of the input
    self.hash = keccak256(_input)

@external
def concatenate_two_hashes(_hash1: bytes32, _hash2: bytes32):
    # Concatenate two hashes and store the result
    self.hash = concat(_hash1, _hash2)

@external
def get_first_half(_hash: bytes32) -> bytes16:
    # Return the first half of the hash
    return slice(_hash, start=0, len=16)

In this example, the compute_keccak256 function computes the keccak256 hash of its input and stores the result. The concatenate_two_hashes function concatenates two bytes32 inputs, and the get_first_half function slices the bytes32 input and returns the first 16 bytes.

Example 3: Identifying Ethereum Addresses

You can also use fixed byte arrays while working with Ethereum addresses. Let’s create a simple contract where an address is stored and retrieved:

ethereum_address: bytes20

@external
def store_address(_address: bytes20):
    # Store an Ethereum address
    self.ethereum_address = _address 

@external
def retrieve_address() -> bytes20:
    # Retrieve the stored address
    return self.ethereum_address

Here, we declare a variable ethereum_address of the type bytes20, suitable for storing an Ethereum address. The store_address function enables storing an Ethereum address, and retrieve_address retrieves the stored address when called.

Best Practices and Potential Pitfalls

While bytesM data types provide a great tool, it’s important to keep some considerations in mind to avoid common pitfalls:

  • Mind the Length: Be cautious about the bytesM length when assigning data. Assigning data of incorrect length will result in an execution error.
  • Storage Space: Opt for the smallest byte size appropriate for your requirements. This helps optimize storage usage and gas costs.
  • Bounds Checking: Be mindful of bounds when using the slice operation or accessing a byte directly. Exceeding byte array bounds will throw an error and halt execution.

The bytesM data type is a crucial ally for developers designing smart contracts in Vyper. Its unique structure offering precise storage makes it a staple in contracts handling fixed-length data. The examples and best practices highlighted in this article aim to provide a good grounding and enable you to navigate the world of fixed-size byte arrays with confidence.

whiteboard crypto logo

WhiteboardCrypto is the #1 online resource for crypto education that explains topics of the cryptocurrency world using analogies, stories, and examples so that anyone can easily understand them. Growing to over 870,000 Youtube subscribers, the content has been shared around the world, played in public conferences and universities, and even in Congress.