Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/book/spell-check-custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,4 @@ callee
decodable
encodable
Vec
hashable
1 change: 1 addition & 0 deletions docs/book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
- [Error Handling](./basics/error_handling.md)
- [Blockchain Development with Sway](./blockchain-development/index.md)
- [Hashing and Cryptography](./blockchain-development/hashing_and_cryptography.md)
- [Implementing the Hash Trait](./blockchain-development/implementing_hash_trait.md)
- [Contract Storage](./blockchain-development/storage.md)
- [Function Purity](./blockchain-development/purity.md)
- [Identifiers](./blockchain-development/identifiers.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The Sway standard library provides easy access to a selection of cryptographic h
{{#include ../../../../examples/hashing/src/main.sw}}
```

To hash your own types, you need to implement the `Hash` trait for them. See
[Implementing the Hash Trait](./implementing_hash_trait.md) for the rules and
examples, including how to safely implement `is_hash_trivial`.

## Cryptographic Signature Recovery and Verification

Fuel supports 3 asymmetric cryptographic signature schemes; `Secp256k1`, `Secp256r1`, and `Ed25519`.
Expand Down
180 changes: 180 additions & 0 deletions docs/book/src/blockchain-development/implementing_hash_trait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Implementing the `Hash` Trait

The `Hash` trait from the standard library (`std::hash::Hash`) defines how a value is hashed. Implementing it for your own types lets you hash them with the `sha256` and `keccak256` functions and use them wherever a deterministic hash is required, for example as keys in a [`StorageMap`](../common-collections/storage_map.md).

Unlike Rust, which can automatically generate a `Hash` implementation via [`#[derive(Hash)]`](https://doc.rust-lang.org/std/hash/trait.Hash.html#derivable), Sway does not currently support deriving trait implementations. The `Hash` trait must therefore always be implemented manually.

## The `Hash` trait

```sway
pub trait Hash {
fn is_hash_trivial() -> bool;

fn hash(self, ref mut state: Hasher);
}
```

An implementation consists of two methods:

- `hash` defines how the value writes its bytes into the `Hasher`. This is the only method that affects the resulting hash value.
- `is_hash_trivial` is an optimization hint. It declares whether the value's *in-memory representation* is byte-for-byte identical to the bytes that `hash` writes into the `Hasher`.

### Implementing `hash`

`hash` writes the *hash bytes representation* of the value into the `Hasher`. For aggregates, this is typically done by hashing each field in turn, and for enums by first hashing a discriminator (the tag) and then the payload:

```sway
use std::hash::{Hash, Hasher};

struct Point {
x: u64,
y: u64,
}

impl Hash for Point {
fn is_hash_trivial() -> bool {
true
}

fn hash(self, ref mut state: Hasher) {
self.x.hash(state);
self.y.hash(state);
}
}
```

### Implementing `is_hash_trivial`

When a type is *trivially hashable*, its hash can be computed directly from the raw memory of the value (the `__size_of::<Self>()` bytes at the value's address), without first building an intermediate byte buffer in a `Hasher`. This is exactly what `sha256` and `keccak256` do for trivially hashable types, and it is significantly more gas efficient.

Returning `true` is a **strong guarantee**: an incorrect `true` will produce wrong hashes when a value is hashed via `sha256` or `keccak256`. Returning `false` is **always safe**; it only forgoes the optimization.

> **When in doubt, return `false`.**

## Rules for safely implementing `is_hash_trivial`

Return `true` only if the in-memory representation of the type is byte-for-byte identical to the bytes its `hash` method writes into the `Hasher`. Several subtleties make types that look trivially hashable actually **not** trivially hashable:

- **`u16` and `u32` are never trivially hashable.** They are stored in memory in an eight-byte slot (as a `u64`), but their hash bytes representation is only two and four bytes, respectively. Any aggregate (struct, tuple, array, ...) containing them is therefore also not trivially hashable.
- **Padding inside aggregates breaks triviality.** `bool`, `u8`, `u16`, and `u32` fields inside a struct or tuple are padded to eight bytes in memory, while `hash` writes them without that padding. An aggregate containing such a field is therefore **not** trivially hashable, even though the field types might be when hashed on their own.
- **Enum tags are stored as `u64`.** The `Hash` implementations in the standard library hash enum tags as `u8`, but the tag is stored as a `u64` in memory. Enums following that convention are therefore **not** trivially hashable.
- **Collections depend on the `new_hashing` feature.** `Bytes`, `Vec`, `raw_slice`, `str`, `str[N]`, arrays, and any aggregate containing them can be trivially hashable or not depending on the [`new_hashing`](https://github.com/FuelLabs/sway/issues/7256) experimental feature. When `new_hashing` is enabled, collections prefix their content with their length, so their hash bytes representation no longer matches their in-memory representation, making them **not** trivially hashable.

A type is trivially hashable when it is a fixed-size type with no padding whose `hash` method writes exactly its in-memory bytes. This includes `u64`, `b256`, `u256`, `bool`, and `()`, as well as structs and tuples whose fields are all
themselves trivially hashable and word-aligned (e.g. only `u64`, `b256`, `u256`).

## Examples

### A trivially hashable struct

A struct whose fields are all word-aligned and trivially hashable, with no padding, is trivially hashable:

```sway
use std::hash::{Hash, Hasher};

struct Stats {
strength: u64,
agility: u64,
}

impl Hash for Stats {
fn is_hash_trivial() -> bool {
// Two `u64` fields, no padding: the in-memory bytes are exactly
// the bytes written by `hash`.
true
}

fn hash(self, ref mut state: Hasher) {
self.strength.hash(state);
self.agility.hash(state);
}
}
```

### A struct that is not trivially hashable

Padded fields (`bool`) and dynamically sized fields (`str`) make a struct not trivially hashable:

```sway
use std::hash::{Hash, Hasher};

struct Account {
id: u64,
active: bool, // Padded to eight bytes in memory.
name: str, // Dynamically sized.
}

impl Hash for Account {
fn is_hash_trivial() -> bool {
// `active` is padded to eight bytes in memory, and `name` is
// dynamically sized, so the in-memory representation does not
// match the hash bytes.
false
}

fn hash(self, ref mut state: Hasher) {
self.id.hash(state);
self.active.hash(state);
self.name.hash(state);
}
}
```

### Enums

Following the standard library convention of hashing the tag as a `u8` makes an enum **not** trivially hashable, because the tag is stored as a `u64` in memory:

```sway
use std::hash::{Hash, Hasher};

enum Shape {
Circle: u64,
Square: u64,
}

impl Hash for Shape {
fn is_hash_trivial() -> bool {
// The tag is hashed as a `u8` but stored as a `u64` in memory.
false
}

fn hash(self, ref mut state: Hasher) {
match self {
Shape::Circle(radius) => {
0_u8.hash(state);
radius.hash(state);
},
Shape::Square(side) => {
1_u8.hash(state);
side.hash(state);
},
}
}
}
```

An enum can be made trivially hashable by hashing the tag as a `u64` (matching its in-memory representation). The simplest safe case is a *tag-only* enum, i.e. an enum whose variants are all unit (zero-sized):

```sway
use std::hash::{Hash, Hasher};

enum Location {
Earth: (),
Mars: (),
}

impl Hash for Location {
fn is_hash_trivial() -> bool {
// The enum consists only of its tag, hashed as a `u64`, which
// matches its in-memory representation.
true
}

fn hash(self, ref mut state: Hasher) {
match self {
Location::Earth => 0_u64.hash(state),
Location::Mars => 1_u64.hash(state),
}
}
}
```
1 change: 1 addition & 0 deletions docs/book/src/blockchain-development/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Sway is fundamentally a blockchain language. Because of this, it has some featur
These are also some concepts related to the FuelVM and Fuel ecosystem that you may utilize when writing Sway.

- [Hashing and Cryptography](./hashing_and_cryptography.md)
- [Implementing the `Hash` Trait](./implementing_hash_trait.md)
- [Contract Storage](./storage.md)
- [Function Purity](./purity.md)
- [Identifiers](./identifiers.md)
Expand Down
22 changes: 20 additions & 2 deletions examples/hashing/src/main.sw
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,44 @@ script;
use std::hash::*;

impl Hash for Location {
fn is_hash_trivial() -> bool {
// `Location` contains only the enum tag
// which is a trivially hashable `u64`.
true
}

fn hash(self, ref mut state: Hasher) {
match self {
Location::Earth => {
0_u8.hash(state);
0_u64.hash(state);
}
Location::Mars => {
1_u8.hash(state);
1_u64.hash(state);
}
}
}
}

impl Hash for Stats {
fn is_hash_trivial() -> bool {
// `Stats` is a struct containing two `u64`s
// and as such trivially hashable.
true
}

fn hash(self, ref mut state: Hasher) {
self.strength.hash(state);
self.agility.hash(state);
}
}

impl Hash for Person {
fn is_hash_trivial() -> bool {
// `Person` is a struct containing a `bool`,
// `str` and array, and as such not trivially hashable.
false
}

fn hash(self, ref mut state: Hasher) {
self.name.hash(state);
self.age.hash(state);
Expand Down
4 changes: 4 additions & 0 deletions sway-lib-std/src/address.sw
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ impl Into<Bytes> for Address {
}

impl Hash for Address {
fn is_hash_trivial() -> bool {
true
}

fn hash(self, ref mut state: Hasher) {
self.bits.hash(state);
}
Expand Down
4 changes: 4 additions & 0 deletions sway-lib-std/src/asset_id.sw
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ pub struct AssetId {
}

impl Hash for AssetId {
fn is_hash_trivial() -> bool {
true
}

fn hash(self, ref mut state: Hasher) {
self.bits.hash(state);
}
Expand Down
6 changes: 6 additions & 0 deletions sway-lib-std/src/b512.sw
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ impl Into<Bytes> for B512 {
}

impl Hash for B512 {
fn is_hash_trivial() -> bool {
// `B512` is just two contiguous `b256`s (64 bytes), **hashed as raw bytes**,
// so its in-memory representation is identical to its hash bytes.
true
}

fn hash(self, ref mut state: Hasher) {
// We want to hash just the raw bytes of the b512,
// and not the `self.bits` array itself.
Expand Down
2 changes: 1 addition & 1 deletion sway-lib-std/src/codec.sw
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,6 @@ impl AbiEncode for str {
#[cfg(experimental_str_array_no_padding = false)]
impl<const N: u64> AbiEncode for str[N] {
// str[N] have alignments and paddings that make them not trivial
// for more information see comments on a test named: string_array
fn is_encode_trivial() -> bool {
false
}
Expand All @@ -306,6 +305,7 @@ impl<const N: u64> AbiEncode for str[N] {
#[cfg(experimental_str_array_no_padding = true)]
impl<const N: u64> AbiEncode for str[N] {
fn is_encode_trivial() -> bool {
// str[N] have no alignments and paddings and are trivial
true
}
fn abi_encode(self, buffer: Buffer) -> Buffer {
Expand Down
4 changes: 4 additions & 0 deletions sway-lib-std/src/contract_id.sw
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ impl Into<Bytes> for ContractId {
}

impl Hash for ContractId {
fn is_hash_trivial() -> bool {
true
}

fn hash(self, ref mut state: Hasher) {
self.bits.hash(state);
}
Expand Down
6 changes: 6 additions & 0 deletions sway-lib-std/src/crypto/ed25519.sw
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@ impl PartialEq for Ed25519 {
impl Eq for Ed25519 {}

impl Hash for Ed25519 {
fn is_hash_trivial() -> bool {
// `Ed25519` is a single inline `[u8; 64]` (64 bytes), hashed as raw
// bytes, so its in-memory representation is identical to its hash bytes.
true
}

fn hash(self, ref mut state: Hasher) {
state.write_raw_slice(raw_slice::from_parts::<u8>(__addr_of(self.bits), 64));
}
Expand Down
4 changes: 4 additions & 0 deletions sway-lib-std/src/crypto/message.sw
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ impl PartialEq for Message {
impl Eq for Message {}

impl Hash for Message {
fn is_hash_trivial() -> bool {
false
}

fn hash(self, ref mut state: Hasher) {
// We want to hash just the raw bytes of the message,
// and not the `self.bytes` `Bytes` itself.
Expand Down
4 changes: 4 additions & 0 deletions sway-lib-std/src/crypto/point2d.sw
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ impl PartialEq for Point2D {
// is not equal to any other point, including itself.

impl Hash for Point2D {
fn is_hash_trivial() -> bool {
false
}

fn hash(self, ref mut state: Hasher) {
self.x.hash(state);
self.y.hash(state);
Expand Down
4 changes: 4 additions & 0 deletions sway-lib-std/src/crypto/public_key.sw
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ impl PartialEq for PublicKey {
impl Eq for PublicKey {}

impl Hash for PublicKey {
fn is_hash_trivial() -> bool {
false
}

fn hash(self, ref mut state: Hasher) {
// We want to hash just the raw bytes of the public key,
// and not the `self.bytes` `Bytes` itself.
Expand Down
4 changes: 4 additions & 0 deletions sway-lib-std/src/crypto/scalar.sw
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ impl PartialEq for Scalar {
// is not equal to any other scalar, including itself.

impl Hash for Scalar {
fn is_hash_trivial() -> bool {
false
}

fn hash(self, ref mut state: Hasher) {
self.bytes.hash(state);
}
Expand Down
6 changes: 6 additions & 0 deletions sway-lib-std/src/crypto/secp256k1.sw
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,12 @@ impl PartialEq for Secp256k1 {
impl Eq for Secp256k1 {}

impl Hash for Secp256k1 {
fn is_hash_trivial() -> bool {
// `Secp256k1` is a single inline `[u8; 64]` (64 bytes), hashed as raw
// bytes, so its in-memory representation is identical to its hash bytes.
true
}

fn hash(self, ref mut state: Hasher) {
// We want to hash just the raw bytes of the signature,
// and not the `self.bits` array itself.
Expand Down
Loading
Loading