Skip to content
Open
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## v1.1.3 - UNRELEASED

### Added

- New function `miss` to verify non-membership for keys using proofs of exclusions.

### Changed

- Fixed proof verification for forks with non-empty prefixes.

### Removed

N/A

## v1.1.2 - 2025-07-12

### Added
Expand Down
5 changes: 2 additions & 3 deletions aiken.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ source = "github"

[[requirements]]
name = "aiken-lang/fuzz"
version = "main"
version = "1.0.0"
source = "github"

[[packages]]
Expand All @@ -19,9 +19,8 @@ source = "github"

[[packages]]
name = "aiken-lang/fuzz"
version = "v1"
version = "1.0.0"
requirements = []
source = "github"

[etags]
"aiken-lang/fuzz@v1" = [{ secs_since_epoch = 1752340228, nanos_since_epoch = 198591000 }, "64b978749e6060a0608706c12db5b430a27082ce344cff942500f269a0ecd7a5"]
2 changes: 1 addition & 1 deletion aiken.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ source = "github"

[[dependencies]]
name = "aiken-lang/fuzz"
version = "main"
version = "1.0.0"
source = "github"
182 changes: 135 additions & 47 deletions lib/aiken/merkle-patricia-forestry.ak
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ pub opaque type MerklePatriciaForestry {
root: ByteArray,
}

// ## Constructing

/// since | <code>1.0.0</code>
/// --- | ---
///
Expand All @@ -76,14 +78,6 @@ pub fn from_root(root: ByteArray) -> MerklePatriciaForestry {
MerklePatriciaForestry { root }
}

/// since | <code>1.1.0</code>
/// --- | ---
///
/// Get the root hash digest of a [MerklePatriciaForestry](#MerklePatriciaForestry).
pub fn root(self: MerklePatriciaForestry) -> ByteArray {
self.root
}

/// since | <code>1.0.0</code>
/// --- | ---
///
Expand All @@ -92,6 +86,8 @@ pub fn empty() -> MerklePatriciaForestry {
MerklePatriciaForestry { root: null_hash }
}

// ## Querying

/// since | <code>1.0.0</code>
/// --- | ---
///
Expand All @@ -111,7 +107,10 @@ pub fn is_empty(self: MerklePatriciaForestry) -> Bool {
/// requires a [Proof](#Proof) of inclusion for the element. The latter can be
/// obtained off-chain from the whole trie containing the element.
///
/// Returns `False` when the element isn't in the tree.
/// > [!CAUTION]
/// > `False` doesn't necessarily indicate that the key IS NOT in the trie.
/// > It only indicates that it isn't present with the given value. To prove
/// > exclusion of a key, use [`miss`](#miss).
pub fn has(
self: MerklePatriciaForestry,
key: ByteArray,
Expand All @@ -121,6 +120,22 @@ pub fn has(
including(key, value, proof) == self.root
}

/// since | <code>1.2.0</code>
/// --- | ---
///
/// Test whether a key is missing from the trie. This requires a
/// [Proof](#Proof) of exclusion for the element. The latter can be obtained
/// off-chain from the whole trie containing the element.
///
/// > [!CAUTION]
/// > `False` doesn't necessarily indicate that the element IS in the trie.
/// > To prove inclusion, use [`has`](#has).
pub fn miss(self: MerklePatriciaForestry, key: ByteArray, proof: Proof) -> Bool {
excluding(key, proof) == self.root
}

// ## Modifying

/// since | <code>1.0.0</code>
/// --- | ---
///
Expand Down Expand Up @@ -152,7 +167,7 @@ pub fn insert(
/// #### Fails when
///
/// - the [Proof](#Proof) is invalid
/// - there's already an element in the trie at the given key
/// - there is no element in the trie at the given key
pub fn delete(
self: MerklePatriciaForestry,
key: ByteArray,
Expand Down Expand Up @@ -183,12 +198,22 @@ pub fn update(
proof: Proof,
old_value: ByteArray,
new_value: ByteArray,
) {
) -> MerklePatriciaForestry {
expect including(key, old_value, proof) == self.root
// If we were doing a delete followed by an insert, we'd end up checking the `excluding` again here
MerklePatriciaForestry { root: including(key, new_value, proof) }
}

// ## Transforming

/// since | <code>1.1.0</code>
/// --- | ---
///
/// Get the root hash digest of a [MerklePatriciaForestry](#MerklePatriciaForestry).
pub fn root(self: MerklePatriciaForestry) -> ByteArray {
self.root
}

// -----------------------------------------------------------------------------
// ----------------------------------------------------------------------- Proof
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -242,6 +267,12 @@ fn including(key: ByteArray, value: ByteArray, proof: Proof) -> ByteArray {
do_including(blake2b_256(key), blake2b_256(value), 0, proof)
}

test including_empty_proof() {
let (k, v) = ("foo", "bar")
let path = blake2b_256(k)
including(k, v, []) == combine(suffix(path, 0), blake2b_256(v))
}

fn do_including(
path: ByteArray,
value: ByteArray,
Expand All @@ -252,29 +283,29 @@ fn do_including(
[] -> combine(suffix(path, cursor), value)

[Branch { skip, neighbors }, ..steps] -> {
let nextCursor = cursor + 1 + skip
let root = do_including(path, value, nextCursor, steps)
do_branch(path, cursor, nextCursor, root, neighbors)
let next_cursor = cursor + skip
let root = do_including(path, value, next_cursor + 1, steps)
do_branch(path, cursor, next_cursor, root, neighbors)
}

[Fork { skip, neighbor }, ..steps] -> {
let nextCursor = cursor + 1 + skip
let root = do_including(path, value, nextCursor, steps)
do_fork(path, cursor, nextCursor, root, neighbor)
let next_cursor = cursor + skip
let root = do_including(path, value, next_cursor + 1, steps)
do_fork(path, cursor, next_cursor, root, neighbor)
}

[Leaf { skip, key, value: neighborValue }, ..steps] -> {
let nextCursor = cursor + 1 + skip
let root = do_including(path, value, nextCursor, steps)
let next_cursor = cursor + skip
let root = do_including(path, value, next_cursor + 1, steps)

let neighbor =
Neighbor {
prefix: suffix(key, nextCursor),
nibble: nibble(key, nextCursor - 1),
prefix: suffix(key, next_cursor + 1),
nibble: nibble(key, next_cursor),
root: neighborValue,
}

do_fork(path, cursor, nextCursor, root, neighbor)
do_fork(path, cursor, next_cursor, root, neighbor)
}
}
}
Expand All @@ -291,14 +322,18 @@ fn excluding(key: ByteArray, proof: Proof) -> ByteArray {
do_excluding(blake2b_256(key), 0, proof)
}

test excluding_empty_proof() {
excluding("foo", []) == empty().root
}

fn do_excluding(path: ByteArray, cursor: Int, proof: Proof) -> ByteArray {
when proof is {
[] -> null_hash

[Branch { skip, neighbors }, ..steps] -> {
let nextCursor = cursor + 1 + skip
let root = do_excluding(path, nextCursor, steps)
do_branch(path, cursor, nextCursor, root, neighbors)
let next_cursor = cursor + skip
let root = do_excluding(path, next_cursor + 1, steps)
do_branch(path, cursor, next_cursor, root, neighbors)
}

[Fork { skip, neighbor }] -> {
Expand All @@ -316,25 +351,25 @@ fn do_excluding(path: ByteArray, cursor: Int, proof: Proof) -> ByteArray {
}

[Fork { skip, neighbor }, ..steps] -> {
let nextCursor = cursor + 1 + skip
let root = do_excluding(path, nextCursor, steps)
do_fork(path, cursor, nextCursor, root, neighbor)
let next_cursor = cursor + skip
let root = do_excluding(path, next_cursor + 1, steps)
do_fork(path, cursor, next_cursor, root, neighbor)
}

[Leaf { key, value, .. }] -> combine(suffix(key, cursor), value)

[Leaf { skip, key, value }, ..steps] -> {
let nextCursor = cursor + 1 + skip
let root = do_excluding(path, nextCursor, steps)
let next_cursor = cursor + skip
let root = do_excluding(path, next_cursor + 1, steps)

let neighbor =
Neighbor {
prefix: suffix(key, nextCursor),
nibble: nibble(key, nextCursor - 1),
prefix: suffix(key, next_cursor + 1),
nibble: nibble(key, next_cursor),
root: value,
}

do_fork(path, cursor, nextCursor, root, neighbor)
do_fork(path, cursor, next_cursor, root, neighbor)
}
}
}
Expand All @@ -346,37 +381,33 @@ fn do_excluding(path: ByteArray, cursor: Int, proof: Proof) -> ByteArray {
fn do_branch(
path: ByteArray,
cursor: Int,
nextCursor: Int,
next_cursor: Int,
root: ByteArray,
neighbors: ByteArray,
) -> ByteArray {
let branch = nibble(path, nextCursor - 1)
let branch = nibble(path, next_cursor)

let prefix = nibbles(path, cursor, next_cursor)

let prefix = nibbles(path, cursor, nextCursor - 1)
let select =
slice_bytearray(_, blake2b_256_digest_size, neighbors)

combine(
prefix,
merkle_16(
branch,
root,
slice_bytearray(0, blake2b_256_digest_size, neighbors),
slice_bytearray(32, blake2b_256_digest_size, neighbors),
slice_bytearray(64, blake2b_256_digest_size, neighbors),
slice_bytearray(96, blake2b_256_digest_size, neighbors),
),
merkle_16(branch, root, select(0), select(32), select(64), select(96)),
)
}

fn do_fork(
path: ByteArray,
cursor: Int,
nextCursor: Int,
next_cursor: Int,
root: ByteArray,
neighbor: Neighbor,
) -> ByteArray {
let branch = nibble(path, nextCursor - 1)
let branch = nibble(path, next_cursor)

let prefix = nibbles(path, cursor, nextCursor - 1)
let prefix = nibbles(path, cursor, next_cursor)

expect branch != neighbor.nibble

Expand All @@ -390,3 +421,60 @@ fn do_fork(
),
)
}

// -----------------------------------------------------------------------------
// -------------------------------------------------- Foreign Function Interface
// -----------------------------------------------------------------------------

/// A version of 'has' that unwraps the opaque arguments, to allow being exported.
fn has_ffi(
self: ByteArray,
key: ByteArray,
value: ByteArray,
proof: Proof,
) -> Bool {
including(key, value, proof) == self
}

test disable_unused_warning_has_ffi() fail {
has_ffi("", "", "", [])
}

/// A version of 'miss' that unwraps the opaque arguments, to allow being exported.
fn miss_ffi(self: ByteArray, key: ByteArray, proof: Proof) -> Bool {
excluding(key, proof) == self
}

test disable_unused_warning_miss_ffi() fail {
miss_ffi("", "", [])
}

/// A version of 'insert' that unwraps the opaque arguments, to allow being exported.
fn insert_ffi(
self: ByteArray,
key: ByteArray,
value: ByteArray,
proof: Proof,
) -> ByteArray {
expect excluding(key, proof) == self
including(key, value, proof)
}

test disable_unused_warning_insert_ffi() fail {
insert_ffi("", "", "", []) == ""
}

/// A version of 'delete' that unwraps the opaque arguments, to allow being exported.
fn delete_ffi(
self: ByteArray,
key: ByteArray,
value: ByteArray,
proof: Proof,
) -> ByteArray {
expect including(key, value, proof) == self
excluding(key, proof)
}

test disable_unused_warning_delete_ffi() fail {
delete_ffi("", "", "", []) == ""
}
14 changes: 14 additions & 0 deletions lib/aiken/merkle-patricia-forestry.tests.ak
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,20 @@ test example_fake_update() fail {

// -------------------- Some notable cases

test example_excluding_empty_proof(key via fuzz.bytearray_between(0, 32)) {
mpf.miss(mpf.empty(), key, [])
}

test example_including_empty_proof(
operands via fuzz.both(
fuzz.bytearray_between(0, 32),
fuzz.bytearray_between(0, 32),
),
) fail {
let (key, value) = operands
mpf.has(mpf.empty(), key, value, [])
}

test example_insert_whatever() {
let root = mpf.insert(without_kiwi(), kiwi, "foo", proof_kiwi())
root != trie()
Expand Down
Loading