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 keccak256
, concat
, 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
:
- 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. - 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
. - 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.
- 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 thekeccak256
hash of the byte arrayx
.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.