Overview

Blinded dictionaries represent a partial reveal of a hash-committed dictionary. Consider the following example: Alice and Bob are playing the game of Battle at Sea. Alice constructs her flotilla as a dictionary of cell coordinates mapped to a boolean value representing presence or absence of a ship. Prior to starting the game, Alice sends Bob a hash commitment to the layout of her flotilla (she can use the _value_id function to retrieve the hash of her dictionary). For each move that Bob makes, Alice is able to provide a blinded dictionary that corresponds in structure to the original flotilla, but only reveals some portion of it, hiding (blinding) the others.

Before you build a blind/reveal protocol, read Salt or hash all keys below. A blinded dictionary necessarily exposes structural information about the keys it commits to; salting or hashing the keys is what keeps that exposure from leaking your secrets.

Constructing a Blinded Dictionary

Given a dictionary A, a blinded dictionary is constructed using the function _construct_blinded_dictionary, whose first argument is A and the second argument is a set of arrays representing sequences of indices into A that must be revealed.

A is any->>any = [[1,2],[3,4]]. 
B = _construct_blinded_dictionary A ([0,0],[1,1]).

In the code above, A is a two-dimensional array, and B is a blinded dictionary that reveals two out of four elements of A, namely, element A[0][0] and A[1][1]

Each path in the reveal set is an array that descends into A (and into any nested dictionaries) to the element to reveal. Every branch not on a revealed path is replaced by its hash value.

Choosing the hash type

_construct_blinded_dictionary commits using the hash type of the source dictionary A (the default canonical hash — see Cryptography Hashes). To commit under an explicit hash type instead, use _construct_blinded_dictionary_custom, which takes a third argument — a metadata record naming the hash type:

B = _construct_blinded_dictionary_custom A ([0,0],[1,1]) ($hash_type -> "S256").

The blinded dictionary commits its root at the named construction type, and it verifies identically regardless of the verifier’s ambient default hash type.

Verifying the Blinded Dictionary

All data in MUFL is hash-addressed, so different blinded dictionaries always have different values of _value_id. In order to match the root hash of a blinded dictionary with the original dictionary, use the function called _root_hash. This _assert will succeed in the context of the above code example:

_assert (_value_id A == _root_hash B).

To verify elements of the blinded dictionary you can use normal subscripting syntax:

_assert (A 1 1 == B 1 1). 
_assert (B 0 1 == NIL). // because element [0,1] is blinded

Note that subscripting a blinded branch returns NIL — the same value a genuinely missing key returns. Subscripting therefore cannot, on its own, distinguish “blinded” from “absent”. To prove that a key is genuinely absent, use proof of absence, described next.

Proving Absence

A blinded dictionary can prove not only that a key is present (with a given value), but also that a key is not present at all. This is possible because each interior node of the committed tree carries a prefix mask — the portion of the keys that all elements under that node share — and the prefix masks are folded into the committed root hash. A revealed mask is therefore authenticated by the same root-hash commitment as the data, so a verifier can trust that a revealed branch diverges from a queried key, which soundly proves the key cannot be present below it.

Constructing a proof of absence

_construct_blinded_with_absence builds a blinded dictionary that simultaneously reveals a set of present elements and proves a set of keys absent. It takes three arguments: the source dictionary, the set of paths to reveal as present, and the set of paths to prove absent:

// reveal A[1][1]; prove that the key path [0,1] is absent
B = _construct_blinded_with_absence A ([1,1]) ([0,1]).

For each absent path, the construction reveals exactly the chain from the root down to the point of divergence (a null branch at the key’s slot, a node whose mask diverges from the key, or a divergent leaf), leaving everything else blinded. That revealed divergence — authenticated by the root hash — is the absence witness.

Examining a blinded dictionary

_examine_blinded classifies a key path against a blinded dictionary. It takes the blinded dictionary and a path (an array of keys descending any nested blinded dictionaries) and returns a two-element array [indicator, value]:

indicator Meaning value
1 Present — the key is revealed with a value the revealed value
-1 Absent — the key is provably not present NIL
0 Unknown — the path descends into a blinded (hash-only) branch, so the result cannot be decided NIL
result = _examine_blinded B [0,1].
_assert (result 0 == (-1)).   // [0,1] is proven absent

Related variants:

  • _examine_blinded_many takes an array of paths and returns an array of [indicator, value] results — bind the dictionary once and examine many keys.
  • _examine_blinded_strict and _examine_blinded_many_strict behave like the above, but instead of returning Absent they fail the transaction if a path descends into a revealed non-dictionary value where a dictionary was expected (a structural mismatch that usually signals a caller error).

Soundness: bind the root hash first

An _examine_blinded verdict is only meaningful after you have bound the blinded dictionary to a trusted root hash. These functions examine the blinded dictionary exactly as supplied; by design they do not check that it corresponds to any committed value. A blinded dictionary is just data — an adversary can fabricate one with a forged structure so that a key reports Absent when it is in fact present (or vice versa). A false Absent is a replay / double-spend class of bug.

Before trusting any result, bind the dictionary to the trusted (e.g. consensus-committed) root hash, in this order:

committed = ... .                                 // the root hash trusted out-of-band
_assert (_root_hash blinded_dict == committed).   // BIND — reject on mismatch
result = _examine_blinded blinded_dict path.       // only now is the verdict sound

There must be no examine-without-bind path in any verifier. Skipping the _root_hash binding makes every absence verdict forgeable.

Salt or hash all keys

A blinded dictionary reveals portions of the keys of blinded branches, and it may leak the presence or absence of branches. The structural prefix masks that make the tree verifiable expose shared key prefixes along every revealed path, and revealing (or proving absent) a branch discloses that a key is present or absent at that position. You must salt or hash all keys in any dictionary intended for the blind/reveal protocol so that these unavoidable structural disclosures reveal nothing about your actual keys and cannot be correlated to guessable values.

In practice, replace each meaningful key with a high-entropy stand-in — for example a hash of the key, or a [key, salt] tuple — so that the revealed prefixes and presence/absence facts carry no usable information. (Recall that dictionaries may serve as keys into dictionaries, so a salted key is naturally expressed as a tuple such as [key, salt].) The same applies to values on blinded branches: salt them as well when their content must remain secret. Keeping a table of salts indexed by the original key lets you reliably reveal the right elements later.