diff --git a/compact-proof-oversized.scale.tar.xz b/compact-proof-oversized.scale.tar.xz new file mode 100644 index 00000000..68f7ab2a Binary files /dev/null and b/compact-proof-oversized.scale.tar.xz differ diff --git a/compact-proof-worst-dedup-child.scale.tar.xz b/compact-proof-worst-dedup-child.scale.tar.xz new file mode 100644 index 00000000..bd53cd6a Binary files /dev/null and b/compact-proof-worst-dedup-child.scale.tar.xz differ diff --git a/test-support/reference-trie/src/lib.rs b/test-support/reference-trie/src/lib.rs index 4eaf5c04..e9892dc4 100644 --- a/test-support/reference-trie/src/lib.rs +++ b/test-support/reference-trie/src/lib.rs @@ -30,6 +30,7 @@ use trie_root::{Hasher, Value as TrieStreamValue}; mod substrate; mod substrate_like; +pub mod trie_db_0_31_decoder; pub mod node { pub use trie_db::node::Node; } diff --git a/test-support/reference-trie/src/trie_db_0_31_decoder.rs b/test-support/reference-trie/src/trie_db_0_31_decoder.rs new file mode 100644 index 00000000..5e7dcbf0 --- /dev/null +++ b/test-support/reference-trie/src/trie_db_0_31_decoder.rs @@ -0,0 +1,208 @@ +// Copyright 2019, 2021 Parity Technologies +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Verbatim compact-proof decoder of the released `trie-db` 0.31.0 (only imports adjusted). +//! +//! A frozen snapshot of deployed decoder behavior — **do not update**. It exists so tests and +//! fuzzers can assert that deduplicated proofs (see `encode_compact_skip_duplicates`) stay +//! decodable by decoders already in the field. Reconstructs into a hash-keyed database only; the +//! 0.31.0 decoder inserts attached values under an incomplete prefix in a position-keyed database +//! (fixed by #227), so prefixed databases are out of scope. + +use hash_db::HashDB; +use std::{convert::TryInto, marker::PhantomData}; +use trie_db::{ + nibble_ops::NIBBLE_LENGTH, + node::{Node, NodeHandle, Value}, + CError, ChildReference, DBValue, NibbleVec, NodeCodec, TrieError, TrieHash, TrieLayout, +}; + +struct DecoderStackEntry<'a, C: NodeCodec> { + node: Node<'a>, + /// The next entry in the stack is a child of the preceding entry at this index. For branch + /// nodes, the index is in [0, NIBBLE_LENGTH] and for extension nodes, the index is in + /// [0, 1]. + child_index: usize, + /// The reconstructed child references. + children: Vec>>, + /// A value attached as a node. The node will need to use its hash as value. + attached_value: Option<&'a [u8]>, + _marker: PhantomData, +} + +impl<'a, C: NodeCodec> DecoderStackEntry<'a, C> { + fn advance_child_index(&mut self) -> trie_db::Result { + match self.node { + Node::Extension(_, child) if self.child_index == 0 => { + match child { + NodeHandle::Inline(data) if data.is_empty() => return Ok(false), + _ => { + let child_ref = child.try_into().map_err(|hash| { + Box::new(TrieError::InvalidHash(C::HashOut::default(), hash)) + })?; + self.children[self.child_index] = Some(child_ref); + }, + } + self.child_index += 1; + }, + Node::Branch(children, _) | Node::NibbledBranch(_, children, _) => { + while self.child_index < NIBBLE_LENGTH { + match children[self.child_index] { + Some(NodeHandle::Inline(data)) if data.is_empty() => return Ok(false), + Some(child) => { + let child_ref = child.try_into().map_err(|hash| { + Box::new(TrieError::InvalidHash(C::HashOut::default(), hash)) + })?; + self.children[self.child_index] = Some(child_ref); + }, + None => {}, + } + self.child_index += 1; + } + }, + _ => {}, + } + Ok(true) + } + + fn push_to_prefix(&self, prefix: &mut NibbleVec) { + match self.node { + Node::Empty => {}, + Node::Leaf(partial, _) | Node::Extension(partial, _) => { + prefix.append_partial(partial.right()); + }, + Node::Branch(_, _) => { + prefix.push(self.child_index as u8); + }, + Node::NibbledBranch(partial, _, _) => { + prefix.append_partial(partial.right()); + prefix.push(self.child_index as u8); + }, + } + } + + fn pop_from_prefix(&self, prefix: &mut NibbleVec) { + match self.node { + Node::Empty => {}, + Node::Leaf(partial, _) | Node::Extension(partial, _) => { + prefix.drop_lasts(partial.len()); + }, + Node::Branch(_, _) => { + prefix.pop(); + }, + Node::NibbledBranch(partial, _, _) => { + prefix.pop(); + prefix.drop_lasts(partial.len()); + }, + } + } + + fn encode_node(self, attached_hash: Option<&[u8]>) -> Vec { + let attached_hash = attached_hash.map(|h| Value::Node(h)); + match self.node { + Node::Empty => C::empty_node().to_vec(), + Node::Leaf(partial, value) => + C::leaf_node(partial.right_iter(), partial.len(), attached_hash.unwrap_or(value)), + Node::Extension(partial, _) => C::extension_node( + partial.right_iter(), + partial.len(), + self.children[0].expect("required by method precondition; qed"), + ), + Node::Branch(_, value) => C::branch_node( + self.children.into_iter(), + if attached_hash.is_some() { attached_hash } else { value }, + ), + Node::NibbledBranch(partial, _, value) => C::branch_node_nibbled( + partial.right_iter(), + partial.len(), + self.children.iter(), + if attached_hash.is_some() { attached_hash } else { value }, + ), + } + } +} + +pub fn decode_compact_from_iter<'a, L, DB, I>( + db: &mut DB, + encoded: I, +) -> trie_db::Result<(TrieHash, usize), TrieHash, CError> +where + L: TrieLayout, + DB: HashDB, + I: IntoIterator, +{ + let mut stack: Vec> = Vec::new(); + + let mut prefix = NibbleVec::new(); + + let mut iter = encoded.into_iter().enumerate(); + while let Some((i, encoded_node)) = iter.next() { + let mut attached_node = 0; + if let Some(header) = L::Codec::ESCAPE_HEADER { + if encoded_node.starts_with(&[header]) { + attached_node = 1; + } + } + let node = L::Codec::decode(&encoded_node[attached_node..]) + .map_err(|err| Box::new(TrieError::DecoderError(>::default(), err)))?; + + let children_len = match node { + Node::Empty | Node::Leaf(..) => 0, + Node::Extension(..) => 1, + Node::Branch(..) | Node::NibbledBranch(..) => NIBBLE_LENGTH, + }; + let mut last_entry = DecoderStackEntry { + node, + child_index: 0, + children: vec![None; children_len], + attached_value: None, + _marker: PhantomData::default(), + }; + + if attached_node > 0 { + // Read value + if let Some((_, fetched_value)) = iter.next() { + last_entry.attached_value = Some(fetched_value); + } else { + return Err(Box::new(TrieError::IncompleteDatabase(>::default()))) + } + } + + loop { + if !last_entry.advance_child_index()? { + last_entry.push_to_prefix(&mut prefix); + stack.push(last_entry); + break + } + + let hash = last_entry + .attached_value + .as_ref() + .map(|value| db.insert(prefix.as_prefix(), value)); + let node_data = last_entry.encode_node(hash.as_ref().map(|h| h.as_ref())); + let node_hash = db.insert(prefix.as_prefix(), node_data.as_ref()); + + if let Some(entry) = stack.pop() { + last_entry = entry; + last_entry.pop_from_prefix(&mut prefix); + last_entry.children[last_entry.child_index] = Some(ChildReference::Hash(node_hash)); + last_entry.child_index += 1; + } else { + return Ok((node_hash, i + 1)) + } + } + } + + Err(Box::new(TrieError::IncompleteDatabase(>::default()))) +} diff --git a/trie-db/CHANGELOG.md b/trie-db/CHANGELOG.md index 07ecc2e8..7f76aabe 100644 --- a/trie-db/CHANGELOG.md +++ b/trie-db/CHANGELOG.md @@ -4,6 +4,18 @@ The format is based on [Keep a Changelog]. [Keep a Changelog]: http://keepachangelog.com/en/1.0.0/ +## [Unreleased] +- Fix the item count returned by `decode_compact`/`decode_compact_from_iter` when the last + decoded node carries an attached (detached-at-encoding) value: the value item was not counted, + so continuing to decode concatenated encodings at the returned offset re-read the value as a + node. +- Add `encode_compact_skip_duplicates`, emitting each distinct item in a compact proof — trie + node or detached value node — only once instead of once per referencing position, and + `decode_compact_from_iter_with_known_items` for decoding across concatenated encodings. + The decoder re-inserts deduplicated items (for subtrees: transitively) at every referencing + position, reconstructing the same database as an encoding without deduplication + ([polkadot-sdk#12565](https://github.com/paritytech/polkadot-sdk/issues/12565)). + ## [0.30.0] - 2025-03-06 - Improve `TrieCache` size by reducing size_of `NodeOwned` [#216](https://github.com/paritytech/trie/pull/216) diff --git a/trie-db/fuzz/Cargo.toml b/trie-db/fuzz/Cargo.toml index 0d92c2a9..d0e223e4 100644 --- a/trie-db/fuzz/Cargo.toml +++ b/trie-db/fuzz/Cargo.toml @@ -10,7 +10,7 @@ cargo-fuzz = true [dependencies] hash-db = { path = "../../hash-db", version = "0.16.0" } -memory-db = { path = "../../memory-db", version = "0.33.0" } +memory-db = { path = "../../memory-db", version = "0.34.0" } reference-trie = { path = "../../test-support/reference-trie", version = "0.29.1" } arbitrary = { version = "1.3.0", features = ["derive"] } array-bytes = "6.0.0" @@ -61,6 +61,14 @@ path = "fuzz_targets/trie_proof_valid.rs" name = "trie_codec_proof" path = "fuzz_targets/trie_codec_proof.rs" +[[bin]] +name = "trie_codec_proof_dedup" +path = "fuzz_targets/trie_codec_proof_dedup.rs" + +[[bin]] +name = "trie_codec_proof_dedup_guided" +path = "fuzz_targets/trie_codec_proof_dedup_guided.rs" + [[bin]] name = "trie_proof_invalid" path = "fuzz_targets/trie_proof_invalid.rs" diff --git a/trie-db/fuzz/fuzz_targets/trie_codec_proof_dedup.rs b/trie-db/fuzz/fuzz_targets/trie_codec_proof_dedup.rs new file mode 100644 index 00000000..2ce0e813 --- /dev/null +++ b/trie-db/fuzz/fuzz_targets/trie_codec_proof_dedup.rs @@ -0,0 +1,18 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use trie_db_fuzz::{ + fuzz_that_trie_codec_proofs, fuzz_that_trie_codec_proofs_with_shared_subtrees, + fuzz_that_trie_codec_proofs_with_shared_values, +}; + +fuzz_target!(|data: &[u8]| { + // Hashed-value layout, so value detachment and deduplication are exercised. + fuzz_that_trie_codec_proofs::>(data); + fuzz_that_trie_codec_proofs_with_shared_values::>( + data, + ); + fuzz_that_trie_codec_proofs_with_shared_subtrees::>( + data, + ); +}); diff --git a/trie-db/fuzz/fuzz_targets/trie_codec_proof_dedup_guided.rs b/trie-db/fuzz/fuzz_targets/trie_codec_proof_dedup_guided.rs new file mode 100644 index 00000000..8951a0d5 --- /dev/null +++ b/trie-db/fuzz/fuzz_targets/trie_codec_proof_dedup_guided.rs @@ -0,0 +1,13 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use trie_db_fuzz::{fuzz_dedup_scenario, DedupScenario}; + +// Structure-aware, differential harness for deduplicated compact proofs. `DedupScenario` is built +// via `arbitrary`, so libFuzzer mutates the trie/value structure directly instead of a raw byte +// stream fed through `fuzz_to_data`. +fuzz_target!(|scenario: DedupScenario| { + // Hashed-value layout: values are detachable value nodes, so value- and subtree-level + // deduplication and the re-insertion machinery are all exercised. + fuzz_dedup_scenario::>(scenario); +}); diff --git a/trie-db/fuzz/src/lib.rs b/trie-db/fuzz/src/lib.rs index d8beb97e..47d4a317 100644 --- a/trie-db/fuzz/src/lib.rs +++ b/trie-db/fuzz/src/lib.rs @@ -218,7 +218,7 @@ pub fn fuzz_prefix_iter(input: &[u8]) { assert_eq!(error, 0); } -#[derive(Debug, Arbitrary)] +#[derive(Debug, Clone, Arbitrary)] pub struct PrefixSeekTestInput { keys: Vec>, prefix_key: Vec, @@ -310,6 +310,61 @@ pub fn fuzz_that_trie_codec_proofs(input: &[u8]) { test_trie_codec_proof::(data, keys); } +pub fn fuzz_that_trie_codec_proofs_with_shared_values(input: &[u8]) { + let mut data = fuzz_to_data(input); + // Draw values from a tiny alphabet of large values, so many keys share a value node. + for (_, value) in data.iter_mut() { + let selector = value.first().copied().unwrap_or(0) % 4; + *value = vec![selector; 64]; + } + // Split data into 3 parts: + // - the first 1/3 is added to the trie and not included in the proof + // - the second 1/3 is added to the trie and included in the proof + // - the last 1/3 is not added to the trie and the proof proves non-inclusion of them + let mut keys = data[(data.len() / 3)..].iter().map(|(key, _)| key.clone()).collect::>(); + data.truncate(data.len() * 2 / 3); + + let data = data_sorted_unique(data); + keys.sort(); + keys.dedup(); + + test_trie_codec_proof::(data, keys); +} + +pub fn fuzz_that_trie_codec_proofs_with_shared_subtrees(input: &[u8]) { + let data = fuzz_to_data(input); + // Mirror every key under two first-nibble prefixes with identical suffixes, drawing values + // from a tiny alphabet of large values, so the trie contains identically encoded sibling + // subtrees (exercising node-level deduplication) referencing shared value nodes (for layouts + // detaching values). + let mut mirrored = Vec::with_capacity(data.len() * 2); + for (key, value) in data { + let value = vec![value.first().copied().unwrap_or(0) % 4; 64]; + for prefix in [0x00u8, 0x10] { + let mut mirrored_key = Vec::with_capacity(key.len() + 1); + mirrored_key.push(prefix); + mirrored_key.extend_from_slice(&key); + mirrored.push((mirrored_key, value.clone())); + } + } + // Split data into 3 parts: + // - the first 1/3 is added to the trie and not included in the proof + // - the second 1/3 is added to the trie and included in the proof + // - the last 1/3 is not added to the trie and the proof proves non-inclusion of them + // Mirrored pairs are adjacent, so the split mostly keeps both occurrences of a subtree. + let mut keys = mirrored[(mirrored.len() / 3)..] + .iter() + .map(|(key, _)| key.clone()) + .collect::>(); + mirrored.truncate(mirrored.len() * 2 / 3); + + let mirrored = data_sorted_unique(mirrored); + keys.sort(); + keys.dedup(); + + test_trie_codec_proof::(mirrored, keys); +} + pub fn fuzz_that_verify_rejects_invalid_proofs(input: &[u8]) { if input.len() < 4 { return @@ -376,9 +431,385 @@ fn test_generate_proof( (root, proof, items) } +/// Structure-aware input for the deduplicating compact-proof harness. +/// +/// Unlike the byte-stream `fuzz_to_data` harnesses (which produce mostly disjoint keys and shallow +/// tries), this describes several tries drawn from a shared value pool, with keys squeezed into a +/// small nibble alphabet and optional (possibly nested) subtree mirroring. That deliberately +/// manufactures colliding keys, deep branches, extensions, nibbled branches carrying a value *and* +/// children, and recursively duplicated subtrees — the structures the deduplication and +/// re-insertion code paths need. Sharing one value pool across tries also exercises cross-encoding +/// deduplication (threaded `seen_hashes` / `known_items`). +#[derive(Debug, Clone, Arbitrary)] +pub struct DedupScenario { + /// The tries in the scenario; capped to [`DedupScenario::MAX_TRIES`] at build time. + tries: Vec, + /// Pool of candidate values, shared by every trie so values (and whole subtrees) collide. + value_pool: Vec, +} + +impl DedupScenario { + const MAX_TRIES: usize = 4; + const MAX_BASE_KEYS: usize = 12; + const MAX_KEY_BYTES: usize = 6; + /// Squeeze raw bytes into a small alphabet so keys collide and branches form. + const KEY_ALPHABET: usize = 4; +} + +#[derive(Debug, Clone, Arbitrary)] +struct TrieSpec { + /// Raw key material; squeezed to a small nibble alphabet at build time. + keys: Vec, + /// Number of identical sibling subtrees to mirror the key set into (`1 + n % 4`, i.e. 1..=4). + /// A value of 1 disables mirroring. + mirror_copies: u8, + /// When set, mirror the key set one extra level down first, so a duplicated subtree itself + /// contains a duplicated sub-subtree (exercises the recursive re-insertion work-loop). + nested_mirror: bool, + /// Indices (modulo entry count) of the keys proven into the partial trie. Empty means "all". + queried: Vec, +} + +#[derive(Debug, Clone, Arbitrary)] +struct KeySpec { + /// Raw key bytes; squeezed to the small alphabet and truncated at build time. + bytes: Vec, + /// Index (modulo pool length) of this key's value in the shared value pool. + value: u16, +} + +#[derive(Debug, Clone, Arbitrary)] +struct ValueSpec { + /// Byte the value is filled with (small alphabet keeps distinct values few and sharing + /// likely). + selector: u8, + /// Large values become separate (detachable) value nodes; small ones stay inline. + large: bool, +} + +/// Map a raw byte to the small key alphabet. Each symbol has equal nibbles so branches diverge at +/// byte boundaries: {0x00, 0x11, 0x22, 0x33}. +fn alphabet_byte(raw: u8) -> u8 { + let symbol = (raw as usize % DedupScenario::KEY_ALPHABET) as u8; + symbol * 0x11 +} + +/// Prepend `prefix` to every key in `entries`, cloning the values. +fn mirror_entries(entries: &[(Vec, Vec)], prefix: u8) -> Vec<(Vec, Vec)> { + entries + .iter() + .map(|(key, value)| { + let mut mirrored = Vec::with_capacity(key.len() + 1); + mirrored.push(prefix); + mirrored.extend_from_slice(key); + (mirrored, value.clone()) + }) + .collect() +} + +/// Materialize the shared value pool. Each spec is either a small inline value or a large value +/// node; an empty pool falls back to one of each. +fn build_value_pool(specs: &[ValueSpec]) -> Vec> { + if specs.is_empty() { + return vec![vec![0u8], vec![1u8; 32]] + } + specs + .iter() + .map(|spec| if spec.large { vec![spec.selector; 32] } else { vec![spec.selector] }) + .collect() +} + +/// Build the sorted, unique `(key, value)` entries for one trie from its spec. +fn build_entries(spec: &TrieSpec, value_pool: &[Vec]) -> Vec<(Vec, Vec)> { + // Base keys, squeezed into the small alphabet, truncated, and non-empty. + let mut base: Vec<(Vec, Vec)> = Vec::new(); + for key_spec in spec.keys.iter().take(DedupScenario::MAX_BASE_KEYS) { + let key: Vec = key_spec + .bytes + .iter() + .take(DedupScenario::MAX_KEY_BYTES) + .map(|raw| alphabet_byte(*raw)) + .collect(); + if key.is_empty() { + continue + } + let value = value_pool[key_spec.value as usize % value_pool.len()].clone(); + base.push((key, value)); + } + + // Optionally mirror one extra level down first, so mirrored subtrees nest. + if spec.nested_mirror { + let mut nested = mirror_entries(&base, 0x01); + nested.extend(mirror_entries(&base, 0x02)); + base = nested; + } + + // Mirror into `copies` identical sibling subtrees below the root (distinct first nibbles). + let copies = 1 + spec.mirror_copies as usize % 4; + let entries = if copies >= 2 { + const OUTER_PREFIXES: [u8; 4] = [0x00, 0x10, 0x20, 0x30]; + let mut mirrored = Vec::new(); + for &prefix in OUTER_PREFIXES.iter().take(copies) { + mirrored.extend(mirror_entries(&base, prefix)); + } + mirrored + } else { + base + }; + + data_sorted_unique(entries) +} + +/// Select the keys proven into the partial trie: the listed indices (modulo entry count), or all +/// keys when none are listed. +fn select_queried(spec: &TrieSpec, entries: &[(Vec, Vec)]) -> Vec> { + if spec.queried.is_empty() { + return entries.iter().map(|(key, _)| key.clone()).collect() + } + let mut keys: Vec> = spec + .queried + .iter() + .map(|index| entries[*index as usize % entries.len()].0.clone()) + .collect(); + keys.sort(); + keys.dedup(); + keys +} + +/// Structure-aware, differential fuzz harness for deduplicated compact proofs. +/// +/// Builds up to [`DedupScenario::MAX_TRIES`] tries, records the queried keys of each, and +/// consolidates all recorded nodes into one frozen union set — the fixed-backing-set +/// precondition of [`encode_compact_skip_duplicates`](trie_db::encode_compact_skip_duplicates) +/// (in Substrate, the single proof recorder shared by a whole block). Every trie is then encoded +/// from that set: plain ([`encode_compact`](trie_db::encode_compact)), self-contained +/// deduplicated (fresh seen-set) and deduplicated with one `seen_hashes` set threaded across all +/// tries. Plain encodings decode independently, threaded ones with a threaded `known_items` map, +/// into both hash-keyed and position-keyed (prefixed) databases. +/// +/// Oracles: +/// - every deduplicated encoding reconstructs its root and all proven keys; +/// - deduplication never grows the encoding (`Σ dedup <= Σ plain`); +/// - re-encoding a fully-seen trie emits only its root ("always emit root"); +/// - a self-contained encoding reconstructs bit-for-bit like plain, both keyings; +/// - a self-contained encoding stays decodable by the released 0.31.0 decoder (hash-keyed); +/// - the threaded reconstruction is sandwiched between plain and the full tries (see +/// [`assert_sub_db`]); strict equality does not hold once subtrees repeat. +/// +/// Threading `seen_hashes` across per-trie recorded sets instead violates the precondition and +/// drops nodes; `divergent_coverage_drops_nodes` in the smoke tests pins that failure mode. +pub fn fuzz_dedup_scenario(scenario: DedupScenario) { + use hash_db::{HashDB, EMPTY_PREFIX}; + use std::collections::{BTreeMap, BTreeSet}; + use trie_db::{ + decode_compact, decode_compact_from_iter_with_known_items, encode_compact, + encode_compact_skip_duplicates, Recorder, + }; + + let value_pool = build_value_pool(&scenario.value_pool); + + // Databases accumulated across all tries for the differential oracle. `expected_*` is the plain + // reconstruction, `actual_*` the threaded deduplicated one, and `full_*` the complete tries — + // the upper bound the reconstruction may never exceed. + let mut expected_prefixed = MemoryDB::, DBValue>::default(); + let mut actual_prefixed = MemoryDB::, DBValue>::default(); + let mut full_prefixed = MemoryDB::, DBValue>::default(); + let mut expected_hashed = MemoryDB::, DBValue>::default(); + let mut actual_hashed = MemoryDB::, DBValue>::default(); + let mut full_hashed = MemoryDB::, DBValue>::default(); + + // Deduplication state threaded across every trie in the scenario. + let mut seen_hashes = BTreeSet::new(); + // Threaded known-items maps: one per reconstructed database. + let mut known_prefixed = BTreeMap::new(); + let mut known_hashed = BTreeMap::new(); + + let mut total_plain = 0usize; + let mut total_dedup = 0usize; + + // Phase 1: build every trie and record its queried keys, consolidating all recorded nodes + // into one frozen union set — so every encoding sees the same coverage below any shared hash + // (the precondition of `encode_compact_skip_duplicates`). + let mut union_partial = MemoryDB::, DBValue>::default(); + let mut recorded = Vec::new(); + for trie_spec in scenario.tries.iter().take(DedupScenario::MAX_TRIES) { + let entries = build_entries(trie_spec, &value_pool); + if entries.is_empty() { + continue + } + + // Build the full trie in a hash-keyed database. + let mut db = MemoryDB::, DBValue>::default(); + let mut root = Default::default(); + { + let mut trie = TrieDBMutBuilder::::new(&mut db, &mut root).build(); + for (key, value) in &entries { + trie.insert(key, value).unwrap(); + } + } + // Accumulate the complete tries (both keyings) as the reconstruction's upper bound. + full_hashed.consolidate(db.clone()); + { + let mut db_prefixed = MemoryDB::, DBValue>::default(); + let mut full_root = Default::default(); + let mut trie = TrieDBMutBuilder::::new(&mut db_prefixed, &mut full_root).build(); + for (key, value) in &entries { + trie.insert(key, value).unwrap(); + } + drop(trie); + full_prefixed.consolidate(db_prefixed); + } + + // Record the partial trie for the proven keys. + let queried = select_queried(trie_spec, &entries); + let mut recorder = Recorder::::new(); + let mut items = Vec::with_capacity(queried.len()); + { + let trie = TrieDBBuilder::::new(&db, &root).with_recorder(&mut recorder).build(); + for key in &queried { + let value = trie.get(key).unwrap(); + items.push((key.clone(), value)); + } + } + for record in recorder.drain() { + union_partial.emplace(record.hash, EMPTY_PREFIX, record.data); + } + recorded.push((root, items)); + } + if recorded.is_empty() { + return + } + + // Phase 2: encode every trie from the frozen union set, threading `seen_hashes` in order. + for (root, items) in &recorded { + // Encode the partial trie three ways: plain, deduplicated with the threaded `seen_hashes`, + // and deduplicated standalone (a fresh seen-set, i.e. a self-contained proof). + let (plain, dedup, standalone) = { + let trie = TrieDBBuilder::::new(&union_partial, root).build(); + let plain = encode_compact::(&trie).unwrap(); + let standalone = + encode_compact_skip_duplicates::(&trie, &mut BTreeSet::new()).unwrap(); + let dedup = encode_compact_skip_duplicates::(&trie, &mut seen_hashes).unwrap(); + (plain, dedup, standalone) + }; + assert!(dedup.len() <= plain.len(), "deduplication must not grow the encoding"); + total_plain += plain.len(); + total_dedup += dedup.len(); + + // Idempotency: with every item already seen, re-encoding emits only the root. + { + let trie = TrieDBBuilder::::new(&union_partial, root).build(); + let reencoded = + encode_compact_skip_duplicates::(&trie, &mut seen_hashes.clone()).unwrap(); + assert_eq!(reencoded.len(), 1, "re-encoding a fully-seen trie must emit only the root"); + } + + // A single self-contained encoding reconstructs exactly like plain (both keyings): within + // one proof only references to already-emitted items are collapsed. The threaded case is + // weaker. + { + let mut plain_hashed = MemoryDB::, DBValue>::default(); + let mut standalone_hashed = MemoryDB::, DBValue>::default(); + decode_compact::(&mut plain_hashed, &plain).unwrap(); + decode_compact::(&mut standalone_hashed, &standalone).unwrap(); + assert!(plain_hashed == standalone_hashed, "single-encoding hash-keyed mismatch"); + + let mut plain_prefixed = MemoryDB::, DBValue>::default(); + let mut standalone_prefixed = MemoryDB::, DBValue>::default(); + decode_compact::(&mut plain_prefixed, &plain).unwrap(); + decode_compact::(&mut standalone_prefixed, &standalone).unwrap(); + assert!(plain_prefixed == standalone_prefixed, "single-encoding prefixed mismatch"); + } + + // Backward compatibility: the released 0.31.0 decoder must still reconstruct a readable + // hash-keyed database from a self-contained encoding (an old node verifying a new proof). + // It under-counts references, so we check readability only, not equality. Hash-keyed + // only: 0.31.0 mis-prefixes attached values in a position-keyed database (fixed by #227). + { + let mut old_db = MemoryDB::, DBValue>::default(); + let (old_root, _) = reference_trie::trie_db_0_31_decoder::decode_compact_from_iter::< + L, + _, + _, + >(&mut old_db, standalone.iter().map(Vec::as_slice)) + .unwrap(); + assert_eq!(&old_root, root, "released 0.31.0 decoder must reconstruct the same root"); + let old_trie = TrieDBBuilder::::new(&old_db, &old_root).build(); + for (key, expected_value) in items { + assert_eq!( + &old_trie.get(key).unwrap(), + expected_value, + "released 0.31.0 decoder must recover every proven key (refcounts aside)" + ); + } + } + + // Decode the plain encoding independently into the expected databases. + let (decoded_root, _) = decode_compact::(&mut expected_hashed, &plain).unwrap(); + assert_eq!(&decoded_root, root); + let (decoded_root, _) = decode_compact::(&mut expected_prefixed, &plain).unwrap(); + assert_eq!(&decoded_root, root); + + // Decode the deduplicated encoding with threaded known-items into the actual databases. + let (decoded_root, _) = decode_compact_from_iter_with_known_items::( + &mut actual_hashed, + dedup.iter().map(Vec::as_slice), + &mut known_hashed, + ) + .unwrap(); + assert_eq!(&decoded_root, root); + let (decoded_root, _) = decode_compact_from_iter_with_known_items::( + &mut actual_prefixed, + dedup.iter().map(Vec::as_slice), + &mut known_prefixed, + ) + .unwrap(); + assert_eq!(&decoded_root, root); + + // Every proven key resolves to its expected value in the reconstructed trie. + let trie = TrieDBBuilder::::new(&actual_hashed, root).build(); + for (key, expected_value) in items { + assert_eq!(&trie.get(key).unwrap(), expected_value); + } + } + + // The threaded, cross-encoding differential. Strict equality does not hold: a deduplicated + // hash reference is indistinguishable from a genuine external one, so identically encoded + // subtrees get re-inserted at extra (genuine) positions. The reconstruction is therefore + // sandwiched — it contains everything plain does and nothing absent from the full tries — and + // over-approximation is the safe direction (a node lingers, never dropped while referenced). + assert_sub_db(&expected_hashed, &actual_hashed, "plain not contained in dedup (hashed)"); + assert_sub_db(&actual_hashed, &full_hashed, "dedup exceeds the full trie (hashed)"); + assert_sub_db(&expected_prefixed, &actual_prefixed, "plain not contained in dedup (prefixed)"); + assert_sub_db(&actual_prefixed, &full_prefixed, "dedup exceeds the full trie (prefixed)"); + assert!(total_dedup <= total_plain); +} + +/// Assert every stored key of `sub` is present in `sup` with a reference count at least as large. +/// Used to sandwich the threaded reconstruction between plain (lower) and the full tries (upper). +fn assert_sub_db(sub: &MemoryDB, sup: &MemoryDB, msg: &str) +where + H: hash_db::Hasher, + KF: memory_db::KeyFunction, + KF::Key: AsRef<[u8]> + Ord, +{ + use std::collections::BTreeMap; + let collect = |db: &MemoryDB| -> BTreeMap, i32> { + db.keys().into_iter().map(|(key, rc)| (key.as_ref().to_vec(), rc)).collect() + }; + let sub = collect(sub); + let sup = collect(sup); + for (key, &sub_rc) in &sub { + match sup.get(key) { + Some(&sup_rc) => assert!(sup_rc >= sub_rc, "{}: reference count under-counted", msg), + None => panic!("{}: key missing from the containing database", msg), + } + } +} + fn test_trie_codec_proof(entries: Vec<(Vec, Vec)>, keys: Vec>) { use hash_db::{HashDB, EMPTY_PREFIX}; - use trie_db::{decode_compact, encode_compact, Recorder}; + use trie_db::{decode_compact, encode_compact, encode_compact_skip_duplicates, Recorder}; // Populate DB with full trie from entries. let (db, root) = { @@ -426,7 +857,39 @@ fn test_trie_codec_proof(entries: Vec<(Vec, Vec)>, keys: // Check that lookups for all items succeed. let trie = TrieDBBuilder::::new(&db, &root).build(); - for (key, expected_value) in items { - assert_eq!(trie.get(key.as_slice()).unwrap(), expected_value); + for (key, expected_value) in &items { + assert_eq!(&trie.get(key.as_slice()).unwrap(), expected_value); + } + + // Round-trip the deduplicating encoding into hash-keyed and prefixed databases. + let deduplicated = { + let trie = TrieDBBuilder::::new(&partial_db, &expected_root).build(); + encode_compact_skip_duplicates::(&trie, &mut Default::default()).unwrap() + }; + assert!(deduplicated.len() <= expected_used); + + let mut hash_keyed_db = , _>>::default(); + let (root, used) = decode_compact::(&mut hash_keyed_db, &deduplicated).unwrap(); + assert_eq!(root, expected_root); + assert_eq!(used, deduplicated.len()); + // Deduplication must not change the reconstructed database (`db` holds the decoded + // unmodified encoding): same entries, same reference counts. + assert!(hash_keyed_db == db && db == hash_keyed_db); + let trie = TrieDBBuilder::::new(&hash_keyed_db, &root).build(); + for (key, expected_value) in &items { + assert_eq!(&trie.get(key.as_slice()).unwrap(), expected_value); + } + + let mut prefixed_db = , _>>::default(); + let (root, _) = decode_compact::(&mut prefixed_db, &deduplicated).unwrap(); + assert_eq!(root, expected_root); + // Same for a position-keyed database, where deduplicated items have to be re-inserted at + // every referencing position. + let mut expected_prefixed_db = , _>>::default(); + decode_compact::(&mut expected_prefixed_db, &compact_trie).unwrap(); + assert!(prefixed_db == expected_prefixed_db && expected_prefixed_db == prefixed_db); + let trie = TrieDBBuilder::::new(&prefixed_db, &root).build(); + for (key, expected_value) in &items { + assert_eq!(&trie.get(key.as_slice()).unwrap(), expected_value); } } diff --git a/trie-db/fuzz/tests/smoke.rs b/trie-db/fuzz/tests/smoke.rs new file mode 100644 index 00000000..7303a10f --- /dev/null +++ b/trie-db/fuzz/tests/smoke.rs @@ -0,0 +1,56 @@ +// Copyright 2026 Parity Technologies +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Deterministic smoke runs of the fuzz harnesses, so their assertions are exercised in CI +//! without a libFuzzer setup. + +use arbitrary::{Arbitrary, Unstructured}; +use reference_trie::HashedValueNoExtThreshold; +use trie_db_fuzz::{ + fuzz_dedup_scenario, fuzz_that_trie_codec_proofs, + fuzz_that_trie_codec_proofs_with_shared_subtrees, + fuzz_that_trie_codec_proofs_with_shared_values, DedupScenario, +}; + +fn xorshift(state: &mut u64) -> u64 { + *state ^= *state << 13; + *state ^= *state >> 7; + *state ^= *state << 17; + *state +} + +#[test] +fn trie_codec_proof_dedup_smoke() { + let mut state = 0x0123_4567_89ab_cdefu64; + for len in (0..2048usize).step_by(37) { + let input: Vec = (0..len).map(|_| xorshift(&mut state) as u8).collect(); + // Hashed-value layout, so value detachment and deduplication are exercised. + fuzz_that_trie_codec_proofs::>(&input); + fuzz_that_trie_codec_proofs_with_shared_values::>(&input); + fuzz_that_trie_codec_proofs_with_shared_subtrees::>(&input); + } +} + +#[test] +fn trie_codec_proof_dedup_guided_smoke() { + let mut state = 0xdead_beef_cafe_f00du64; + for len in (0..4096usize).step_by(29) { + let input: Vec = (0..len).map(|_| xorshift(&mut state) as u8).collect(); + // Structure-aware scenario built the same way libFuzzer builds it, via `arbitrary`. + let mut unstructured = Unstructured::new(&input); + if let Ok(scenario) = DedupScenario::arbitrary(&mut unstructured) { + fuzz_dedup_scenario::>(scenario); + } + } +} diff --git a/trie-db/src/iterator.rs b/trie-db/src/iterator.rs index f8b9381b..dd9c7457 100644 --- a/trie-db/src/iterator.rs +++ b/trie-db/src/iterator.rs @@ -125,6 +125,16 @@ impl TrieDBRawIterator { .push(Crumb { hash: node_hash, status: Status::Entering, node: Arc::new(node) }); } + /// Skip the descendants of the node most recently yielded by `next_raw_item`: iteration + /// continues with the node's next sibling (or an ancestor's). + /// + /// Must only be called directly after `next_raw_item(_, true)` yielded a node. + pub(crate) fn skip_current_subtree(&mut self) { + if let Some(crumb) = self.trail.last_mut() { + crumb.status = Status::AftExiting; + } + } + /// Fetch value by hash at a current node height pub(crate) fn fetch_value( db: &TrieDB, diff --git a/trie-db/src/lib.rs b/trie-db/src/lib.rs index 2c091eaa..baf66fee 100644 --- a/trie-db/src/lib.rs +++ b/trie-db/src/lib.rs @@ -22,7 +22,7 @@ extern crate alloc; mod rstd { pub use std::{ borrow, boxed, cmp, - collections::{BTreeMap, VecDeque}, + collections::{BTreeMap, BTreeSet, VecDeque}, convert, error::Error, fmt, hash, iter, marker, mem, ops, result, sync, vec, @@ -33,7 +33,7 @@ mod rstd { mod rstd { pub use alloc::{ borrow, boxed, - collections::{btree_map::BTreeMap, VecDeque}, + collections::{btree_map::BTreeMap, btree_set::BTreeSet, VecDeque}, rc, sync, vec, }; pub use core::{cmp, convert, fmt, hash, iter, marker, mem, ops, result}; @@ -81,7 +81,10 @@ pub use crate::{ iter_build::{trie_visit, ProcessEncodedNode, TrieBuilder, TrieRoot, TrieRootUnhashed}, iterator::{TrieDBNodeIterator, TrieDBRawIterator}, node_codec::{NodeCodec, Partial}, - trie_codec::{decode_compact, decode_compact_from_iter, encode_compact}, + trie_codec::{ + decode_compact, decode_compact_from_iter, decode_compact_from_iter_with_known_items, + encode_compact, encode_compact_skip_duplicates, + }, }; pub use hash_db::{HashDB, HashDBRef, Hasher}; diff --git a/trie-db/src/trie_codec.rs b/trie-db/src/trie_codec.rs index da7af4ed..25740703 100644 --- a/trie-db/src/trie_codec.rs +++ b/trie-db/src/trie_codec.rs @@ -24,15 +24,29 @@ //! hash references to nodes not in the partial trie are left intact. The compact encoding can be //! expected to save roughly (n - 1) hashes in size where n is the number of nodes in the partial //! trie. +//! +//! A value node contained in the partial trie (see [`TrieLayout::MAX_INLINE_VALUE`]) is +//! "detached": the referencing node is emitted with an escape header (see +//! `NodeCodec::ESCAPE_HEADER`) and an empty inline value, directly followed by the value bytes +//! as a standalone item. A node whose value node is *not* part of the partial trie is emitted +//! unmodified, still referencing its value by hash. +//! +//! `encode_compact` re-emits a shared item once per position (a detached value per referencing +//! node, a duplicated subtree per occurrence). [`encode_compact_skip_duplicates`] instead emits +//! each distinct item only once; later occurrences keep a plain hash reference, like any +//! reference to an item outside the partial trie. use crate::{ nibble_ops::NIBBLE_LENGTH, - node::{Node, NodeHandle, NodeHandlePlan, NodePlan, OwnedNode, ValuePlan}, - rstd::{boxed::Box, convert::TryInto, marker::PhantomData, result, sync::Arc, vec, vec::Vec}, + node::{Node, NodeHandle, NodeHandlePlan, NodePlan, OwnedNode, Value, ValuePlan}, + rstd::{ + boxed::Box, convert::TryInto, marker::PhantomData, result, sync::Arc, vec, vec::Vec, + BTreeMap, BTreeSet, + }, CError, ChildReference, DBValue, NibbleVec, NodeCodec, Result, TrieDB, TrieDBRawIterator, TrieError, TrieHash, TrieLayout, }; -use hash_db::{HashDB, Prefix}; +use hash_db::{HashDB, Hasher, Prefix}; const OMIT_VALUE_HASH: crate::node::Value<'static> = crate::node::Value::Inline(&[]); @@ -187,24 +201,32 @@ impl EncoderStackEntry { /// Detached value if included does write a reserved header, /// followed by node encoded with 0 length value and the value /// as a standalone vec. +/// +/// When `seen_hashes` is given, a value whose hash is already in the set is not detached again. +/// Only hashes of actually emitted values are added to the set. fn detached_value( db: &TrieDB, value: &ValuePlan, node_data: &[u8], node_prefix: Prefix, + seen_hashes: Option<&mut BTreeSet>>, ) -> Option> { - let fetched; - match value { - ValuePlan::Node(hash_plan) => { - if let Ok(value) = - TrieDBRawIterator::fetch_value(db, &node_data[hash_plan.clone()], node_prefix) - { - fetched = value; - } else { - return None - } - }, + let hash_plan = match value { + ValuePlan::Node(hash_plan) => hash_plan, _ => return None, + }; + let value_hash = &node_data[hash_plan.clone()]; + if let Some(seen_hashes) = &seen_hashes { + if seen_hashes.contains(value_hash) { + return None + } + } + let fetched = match TrieDBRawIterator::fetch_value(db, value_hash, node_prefix) { + Ok(value) => value, + Err(_) => return None, + }; + if let Some(seen_hashes) = seen_hashes { + seen_hashes.insert(value_hash.to_vec()); } Some(fetched) } @@ -214,9 +236,50 @@ fn detached_value( /// are listed in pre-order traversal order so that the full nodes can be efficiently /// reconstructed recursively. /// +/// A shared detached value node is emitted once per referencing node and a duplicated subtree +/// once per occurrence (see [`encode_compact_skip_duplicates`]). +/// /// This function makes the assumption that all child references in an inline trie node are inline /// references. pub fn encode_compact(db: &TrieDB) -> Result>, TrieHash, CError> +where + L: TrieLayout, +{ + encode_compact_inner(db, None) +} + +/// Variant of [`encode_compact`] that emits each distinct item — trie node or detached value +/// node — only once. Later occurrences keep a plain hash reference to the emitted item, exactly +/// like references to items outside the partial trie. Only items at least as large as a hash are +/// referenced this way, so deduplication never grows the encoding. +/// +/// `seen_hashes` collects the emitted hashes. +/// +/// All encodings sharing one `seen_hashes` set must be generated from a single, fixed backing +/// node set: a skipped subtree is reconstructable only if everything below it was emitted when +/// its root was first seen. Encoding from per-proof recorded sets whose coverage of a shared +/// node diverges silently drops the divergent nodes and produces unverifiable proofs. +/// +/// Any decoder reconstructs a readable hash-keyed database from the result. Reconstructing the +/// exact database of an un-deduplicated encoding (every position populated, matching reference +/// counts) requires a re-inserting decoder, i.e. [`decode_compact_from_iter_with_known_items`]. +/// +/// Assumes occurrences of an item are interchangeable, as they are when `db` is hash-keyed. A +/// prefixed `db` populating a duplicated subtree only below a later occurrence would drop it. +pub fn encode_compact_skip_duplicates( + db: &TrieDB, + seen_hashes: &mut BTreeSet>, +) -> Result>, TrieHash, CError> +where + L: TrieLayout, +{ + encode_compact_inner(db, Some(seen_hashes)) +} + +fn encode_compact_inner( + db: &TrieDB, + mut seen_hashes: Option<&mut BTreeSet>>, +) -> Result>, TrieHash, CError> where L: TrieLayout, { @@ -241,8 +304,21 @@ where Ok((prefix, node_hash, node)) => { // Skip inline nodes, as they cannot contain hash references to other nodes by // assumption. - if node_hash.is_none() { - continue + let Some(node_hash) = node_hash else { continue }; + + if let Some(seen_hashes) = seen_hashes.as_deref_mut() { + let is_root = stack.is_empty(); + // A subtree whose root was already emitted is skipped entirely; the parent's + // `omit_children` bit stays unset, keeping a plain hash reference. The root is + // never skipped, so each encoding stays individually decodable when + // `seen_hashes` is threaded across successive encodings. Sound only under the + // fixed-backing-set precondition (see `encode_compact_skip_duplicates`): the + // subtree below a seen hash must not have grown since it was emitted. + if !is_root && seen_hashes.contains(node_hash.as_ref()) { + iter.skip_current_subtree(); + continue + } + seen_hashes.insert(node_hash.as_ref().to_vec()); } // Unwind the stack until the new entry is a child of the last entry on the stack. @@ -268,12 +344,28 @@ where let (children_len, detached_value) = match node.node_plan() { NodePlan::Empty => (0, None), - NodePlan::Leaf { value, .. } => - (0, detached_value(db, value, node.data(), prefix.as_prefix())), + NodePlan::Leaf { value, .. } => ( + 0, + detached_value( + db, + value, + node.data(), + prefix.as_prefix(), + seen_hashes.as_deref_mut(), + ), + ), NodePlan::Extension { .. } => (1, None), NodePlan::NibbledBranch { value: Some(value), .. } | - NodePlan::Branch { value: Some(value), .. } => - (NIBBLE_LENGTH, detached_value(db, value, node.data(), prefix.as_prefix())), + NodePlan::Branch { value: Some(value), .. } => ( + NIBBLE_LENGTH, + detached_value( + db, + value, + node.data(), + prefix.as_prefix(), + seen_hashes.as_deref_mut(), + ), + ), NodePlan::NibbledBranch { value: None, .. } | NodePlan::Branch { value: None, .. } => (NIBBLE_LENGTH, None), }; @@ -318,6 +410,9 @@ struct DecoderStackEntry<'a, C: NodeCodec> { child_index: usize, /// The reconstructed child references. children: Vec>>, + /// Bit mask of children kept as plain hash references, which may point at subtrees + /// deduplicated into an earlier occurrence (see [`encode_compact_skip_duplicates`]). + hash_ref_children: u16, /// A value attached as a node. The node will need to use its hash as value. attached_value: Option<&'a [u8]>, _marker: PhantomData, @@ -338,9 +433,12 @@ impl<'a, C: NodeCodec> DecoderStackEntry<'a, C> { match child { NodeHandle::Inline(data) if data.is_empty() => return Ok(false), _ => { - let child_ref = child.try_into().map_err(|hash| { + let child_ref: ChildReference<_> = child.try_into().map_err(|hash| { Box::new(TrieError::InvalidHash(C::HashOut::default(), hash)) })?; + if child_ref.is_hash() { + self.hash_ref_children |= 1u16 << self.child_index; + } self.children[self.child_index] = Some(child_ref); }, } @@ -351,9 +449,13 @@ impl<'a, C: NodeCodec> DecoderStackEntry<'a, C> { match children[self.child_index] { Some(NodeHandle::Inline(data)) if data.is_empty() => return Ok(false), Some(child) => { - let child_ref = child.try_into().map_err(|hash| { - Box::new(TrieError::InvalidHash(C::HashOut::default(), hash)) - })?; + let child_ref: ChildReference<_> = + child.try_into().map_err(|hash| { + Box::new(TrieError::InvalidHash(C::HashOut::default(), hash)) + })?; + if child_ref.is_hash() { + self.hash_ref_children |= 1u16 << self.child_index; + } self.children[self.child_index] = Some(child_ref); }, None => {}, @@ -431,6 +533,128 @@ impl<'a, C: NodeCodec> DecoderStackEntry<'a, C> { } } +/// Re-insert the deduplicated subtrees that `entry`'s node references through plain hash-reference +/// children (see [`encode_compact_skip_duplicates`]), replaying what an un-deduplicated encoding +/// would have inserted below this node. Hashes missing from `known_items` reference items outside +/// the encoding and are skipped, like the holes an un-deduplicated encoding would leave. +/// +/// `prefix` is `entry`'s node prefix; it is restored before returning. +fn reinsert_known_subtrees( + db: &mut DB, + known_items: &BTreeMap, DBValue>, + entry: &DecoderStackEntry, + prefix: &mut NibbleVec, +) -> Result<(), TrieHash, CError> +where + L: TrieLayout, + DB: HashDB, +{ + let partial_len = match &entry.node { + Node::Extension(partial, _) | Node::NibbledBranch(partial, _, _) => { + prefix.append_partial(partial.right()); + partial.len() + }, + _ => 0, + }; + for index in 0..entry.children.len() { + if entry.hash_ref_children & (1u16 << index) == 0 { + continue + } + let hash = match &entry.children[index] { + Some(ChildReference::Hash(hash)) => hash, + _ => continue, + }; + // An extension's child sits directly below the partial; branch children add their nibble. + let result = if matches!(&entry.node, Node::Extension(..)) { + reinsert_known_subtree::(db, known_items, hash.as_ref(), prefix) + } else { + prefix.push(index as u8); + let result = reinsert_known_subtree::(db, known_items, hash.as_ref(), prefix); + prefix.pop(); + result + }; + result?; + } + prefix.drop_lasts(partial_len); + Ok(()) +} + +/// Re-insert at `prefix` the subtree rooted at the known node with `hash`: the node, its known +/// detached value and, transitively, its known child subtrees, each at the position it would +/// occupy below `prefix`. +fn reinsert_known_subtree( + db: &mut DB, + known_items: &BTreeMap, DBValue>, + hash: &[u8], + prefix: &NibbleVec, +) -> Result<(), TrieHash, CError> +where + L: TrieLayout, + DB: HashDB, +{ + // Work items are `(prefix, hash, is_value)`. Values are inserted verbatim; nodes are decoded + // to enqueue their hash-referenced value and children. A decode failure only means the hash + // was recorded for a value whose bytes are not a node, so it is ignored rather than an error. + let mut work = vec![(prefix.clone(), hash.to_vec(), false)]; + while let Some((prefix, hash, is_value)) = work.pop() { + let item = match known_items.get(&hash) { + Some(item) => item, + None => continue, + }; + if is_value { + db.insert(prefix.as_prefix(), item); + continue + } + let node = match L::Codec::decode(item) { + Ok(node) => node, + Err(_) => continue, + }; + db.insert(prefix.as_prefix(), item); + match node { + Node::Empty => {}, + Node::Leaf(partial, value) => + if let Value::Node(value_hash) = value { + let mut value_prefix = prefix; + value_prefix.append_partial(partial.right()); + work.push((value_prefix, value_hash.to_vec(), true)); + }, + Node::Extension(partial, child) => + if let NodeHandle::Hash(child_hash) = child { + let mut child_prefix = prefix; + child_prefix.append_partial(partial.right()); + work.push((child_prefix, child_hash.to_vec(), false)); + }, + Node::Branch(children, value) => { + if let Some(Value::Node(value_hash)) = value { + work.push((prefix.clone(), value_hash.to_vec(), true)); + } + for (index, child) in children.iter().enumerate() { + if let Some(NodeHandle::Hash(child_hash)) = child { + let mut child_prefix = prefix.clone(); + child_prefix.push(index as u8); + work.push((child_prefix, child_hash.to_vec(), false)); + } + } + }, + Node::NibbledBranch(partial, children, value) => { + let mut node_prefix = prefix; + node_prefix.append_partial(partial.right()); + if let Some(Value::Node(value_hash)) = value { + work.push((node_prefix.clone(), value_hash.to_vec(), true)); + } + for (index, child) in children.iter().enumerate() { + if let Some(NodeHandle::Hash(child_hash)) = child { + let mut child_prefix = node_prefix.clone(); + child_prefix.push(index as u8); + work.push((child_prefix, child_hash.to_vec(), false)); + } + } + }, + } + } + Ok(()) +} + /// Reconstructs a partial trie DB from a compact representation. The encoding is a vector of /// mutated trie nodes with those child references omitted. The decode function reads them in order /// from the given slice, reconstructing the full nodes and inserting them into the given `HashDB`. @@ -459,6 +683,39 @@ pub fn decode_compact_from_iter<'a, L, DB, I>( db: &mut DB, encoded: I, ) -> Result<(TrieHash, usize), TrieHash, CError> +where + L: TrieLayout, + DB: HashDB, + I: IntoIterator, +{ + decode_compact_from_iter_with_known_items::(db, encoded, &mut BTreeMap::new()) +} + +/// Variant of [`decode_compact_from_iter`] that threads the map of already decoded items — +/// attached values and reconstructed nodes, keyed by hash — across successive calls. +/// +/// Every attached value and reconstructed node is recorded in `known_items`. A node referencing a +/// known item only by hash (see [`encode_compact_skip_duplicates`]) has it — for a subtree, the +/// item and everything known below it — re-inserted at that position. Threading across calls is +/// only needed for encodings deduplicated with a shared `seen_hashes` set. +/// +/// A deduplicated hash reference is indistinguishable from a genuine reference to a node outside +/// the encoding, so when the trie has identically encoded subtrees this re-inserts external +/// siblings too. The result is a *superset* of the un-deduplicated database: same nodes, but +/// reference counts that may be higher (bounded by the full trie), never lower. Reads and the root +/// are exact; over-counting is the safe direction for reference-counted stores. +/// +/// Decoding work is proportional to the *un-deduplicated* encoding: re-inserting subtrees at every +/// occurrence can touch far more positions than there are items in `encoded`, so callers decoding +/// untrusted input should bound the logical size, not the encoded size. +/// +/// Returns the root hash of the reconstructed partial trie and the number of items consumed from +/// `encoded`. +pub fn decode_compact_from_iter_with_known_items<'a, L, DB, I>( + db: &mut DB, + encoded: I, + known_items: &mut BTreeMap, DBValue>, +) -> Result<(TrieHash, usize), TrieHash, CError> where L: TrieLayout, DB: HashDB, @@ -491,6 +748,7 @@ where node, child_index: 0, children: vec![None; children_len], + hash_ref_children: 0, attached_value: None, _marker: PhantomData::default(), }; @@ -499,6 +757,10 @@ where // Read value if let Some((_, fetched_value)) = iter.next() { last_entry.attached_value = Some(fetched_value); + // Record immediately: a node deduplicated against this one can complete before + // this node does, e.g. a leaf below a branch carrying the value. + known_items + .insert(L::Hash::hash(fetched_value).as_ref().to_vec(), fetched_value.to_vec()); } else { return Err(Box::new(TrieError::IncompleteDatabase(>::default()))) } @@ -525,16 +787,47 @@ where prefix.drop_lasts(partial_prefix_len); hash }); + if hash.is_none() { + // The node's detached value may have been deduplicated into an earlier occurrence + // (see `encode_compact_skip_duplicates`); re-insert it here too so `db` matches an + // un-deduplicated encoding. A miss means the value is not in the encoding, which is + // legal. + let value_hash = match &last_entry.node { + Node::Leaf(_, Value::Node(hash)) => Some(*hash), + Node::Branch(_, Some(Value::Node(hash))) | + Node::NibbledBranch(_, _, Some(Value::Node(hash))) => Some(*hash), + _ => None, + }; + if let Some(value) = value_hash.and_then(|hash| known_items.get(hash)) { + let partial_prefix_len = match &last_entry.node { + Node::Leaf(partial, _) | Node::NibbledBranch(partial, _, _) => { + prefix.append_partial(partial.right()); + partial.len() + }, + _ => 0, + }; + db.insert(prefix.as_prefix(), value); + prefix.drop_lasts(partial_prefix_len); + } + } + // Children kept as plain hash references may point at subtrees deduplicated into an + // earlier occurrence; re-insert them here too. Misses again mean the nodes are not in + // the encoding. + if last_entry.hash_ref_children != 0 { + reinsert_known_subtrees::(db, known_items, &last_entry, &mut prefix)?; + } let node_data = last_entry.encode_node(hash.as_ref().map(|h| h.as_ref())); let node_hash = db.insert(prefix.as_prefix(), node_data.as_ref()); + known_items.insert(node_hash.as_ref().to_vec(), node_data); + if let Some(entry) = stack.pop() { last_entry = entry; last_entry.pop_from_prefix(&mut prefix); last_entry.children[last_entry.child_index] = Some(ChildReference::Hash(node_hash)); last_entry.child_index += 1; } else { - return Ok((node_hash, i + 1)) + return Ok((node_hash, i + 1 + attached_node)) } } } diff --git a/trie-db/src/triedbmut.rs b/trie-db/src/triedbmut.rs index ba35aed0..15b4cbc9 100644 --- a/trie-db/src/triedbmut.rs +++ b/trie-db/src/triedbmut.rs @@ -564,6 +564,13 @@ pub enum ChildReference { Inline(HO, usize), // usize is the length of the node data we store in the `H::Out` } +impl ChildReference { + /// Returns true if this is a hash reference + pub fn is_hash(&self) -> bool { + matches!(self, ChildReference::Hash(_)) + } +} + impl<'a, HO> TryFrom> for ChildReference where HO: AsRef<[u8]> + AsMut<[u8]> + Default + Clone + Copy, diff --git a/trie-db/test/src/trie_codec.rs b/trie-db/test/src/trie_codec.rs index cd77032e..84d758eb 100644 --- a/trie-db/test/src/trie_codec.rs +++ b/trie-db/test/src/trie_codec.rs @@ -14,8 +14,10 @@ use hash_db::{HashDB, HashDBRef, Hasher, EMPTY_PREFIX}; use reference_trie::{test_layouts, ExtensionLayout}; +use std::collections::{BTreeMap, BTreeSet}; use trie_db::{ - decode_compact, encode_compact, DBValue, NodeCodec, Recorder, Trie, TrieDBBuilder, + decode_compact, decode_compact_from_iter_with_known_items, encode_compact, + encode_compact_skip_duplicates, DBValue, NodeCodec, Recorder, Trie, TrieDBBuilder, TrieDBMutBuilder, TrieError, TrieLayout, TrieMut, }; @@ -152,6 +154,397 @@ fn trie_decoding_fails_with_incomplete_database_internal() { } } +/// A value above every tested layout threshold, stored as a shared, hash-addressed value node. +const SHARED_VALUE: &[u8] = &[4; 32]; + +/// Whether the layout stores [`SHARED_VALUE`] as a separate value node. +fn has_value_nodes() -> bool { + T::MAX_INLINE_VALUE.map_or(false, |threshold| threshold as usize <= SHARED_VALUE.len()) +} + +/// Count the encoded items that consist of exactly the shared value, i.e. its detached copies. +fn count_shared_value_items(encoded: &[Vec]) -> usize { + encoded.iter().filter(|item| item.as_slice() == SHARED_VALUE).count() +} + +/// Entries where five nodes (one branch with value, four leaves) reference the shared value. +fn shared_value_entries() -> Vec<(&'static [u8], &'static [u8])> { + vec![ + // "key" is at a branch node carrying the shared value. + (b"key", SHARED_VALUE), + (b"key1", SHARED_VALUE), + (b"key2", SHARED_VALUE), + (b"key3", SHARED_VALUE), + // A distinct value node, must not be affected by deduplication. + (b"key4", &[5; 32]), + // A leaf in an unrelated part of the trie, referencing the shared value. + (b"other", SHARED_VALUE), + ] +} + +fn build_trie( + entries: &[(&'static [u8], &'static [u8])], +) -> (MemoryDB, ::Out) { + let mut db = >::default(); + let mut root = Default::default(); + { + let mut trie = >::new(&mut db, &mut root).build(); + for (key, value) in entries.iter() { + trie.insert(key, value).unwrap(); + } + } + (db, root) +} + +fn assert_entries_match( + db: &(impl HashDB + HashDBRef), + root: ::Out, + entries: &[(&'static [u8], &'static [u8])], +) { + let trie = >::new(db, &root).build(); + for (key, value) in entries.iter() { + assert_eq!(trie.get(key).unwrap().as_deref(), Some(*value)); + } +} + +test_layouts!( + skip_duplicate_values_emits_shared_values_once, + skip_duplicate_values_emits_shared_values_once_internal +); +fn skip_duplicate_values_emits_shared_values_once_internal() { + let entries = shared_value_entries(); + let (db, root) = build_trie::(&entries); + + let trie = >::new(&db, &root).build(); + let encoded = encode_compact::(&trie).unwrap(); + let deduplicated = encode_compact_skip_duplicates::(&trie, &mut Default::default()).unwrap(); + + if has_value_nodes::() { + // One detached copy per referencing node without deduplication... + assert_eq!(count_shared_value_items(&encoded), 5); + // ...exactly one with it. + assert_eq!(count_shared_value_items(&deduplicated), 1); + // Four duplicate value copies are skipped, and the leaves of "key2" and "key3" — encoded + // identically to the "key1" leaf (same partial, same value hash) — are deduplicated at + // the node level. + assert_eq!(deduplicated.len(), encoded.len() - 4 - 2); + } else { + // Without value nodes there are no detached values to deduplicate, but the identical + // leaves of "key2" and "key3" still are. + assert_eq!(count_shared_value_items(&encoded), 0); + assert_eq!(deduplicated.len(), encoded.len() - 2); + } + + // The deduplicated encoding reconstructs a fully readable trie in a hash-keyed database... + let mut hash_keyed_db = MemoryDB::::default(); + let (decoded_root, used) = decode_compact::(&mut hash_keyed_db, &deduplicated).unwrap(); + assert_eq!(decoded_root, root); + assert_eq!(used, deduplicated.len()); + assert_entries_match::(&hash_keyed_db, root, &entries); + + // ...and, via value re-insertion, in a position-keyed (prefixed) database as well. + let mut prefixed_db = PrefixedMemoryDB::::default(); + let (decoded_root, _) = decode_compact::(&mut prefixed_db, &deduplicated).unwrap(); + assert_eq!(decoded_root, root); + assert_entries_match::(&prefixed_db, root, &entries); + + // Both encodings reconstruct identical databases: same entries, same reference counts. + let mut expected_hash_keyed_db = MemoryDB::::default(); + decode_compact::(&mut expected_hash_keyed_db, &encoded).unwrap(); + assert!(hash_keyed_db == expected_hash_keyed_db && expected_hash_keyed_db == hash_keyed_db); + let mut expected_prefixed_db = PrefixedMemoryDB::::default(); + decode_compact::(&mut expected_prefixed_db, &encoded).unwrap(); + assert!(prefixed_db == expected_prefixed_db && expected_prefixed_db == prefixed_db); +} + +test_layouts!( + skip_duplicate_values_handles_missing_value_nodes, + skip_duplicate_values_handles_missing_value_nodes_internal +); +fn skip_duplicate_values_handles_missing_value_nodes_internal() { + let entries = shared_value_entries(); + let (db, root) = build_trie::(&entries); + + // Record all keys, but leave the shared value node out of the partial trie. + let mut recorder = Recorder::::new(); + { + let trie = >::new(&db, &root).with_recorder(&mut recorder).build(); + for (key, _) in entries.iter() { + trie.get(key).unwrap(); + } + } + let mut partial_db = MemoryDB::::default(); + for record in recorder.drain() { + if record.data != SHARED_VALUE { + partial_db.insert(EMPTY_PREFIX, &record.data); + } + } + + let trie = >::new(&partial_db, &root).build(); + let encoded = encode_compact::(&trie).unwrap(); + let deduplicated = encode_compact_skip_duplicates::(&trie, &mut Default::default()).unwrap(); + + // With no fetchable shared value there is nothing to detach: no encoded item is a standalone + // value and the referencing nodes are emitted unmodified. The leaves of "key2" and "key3" — + // encoded identically to the "key1" leaf — are still deduplicated at the node level. + assert_eq!(count_shared_value_items(&encoded), 0); + assert_eq!(count_shared_value_items(&deduplicated), 0); + assert_eq!(deduplicated.len(), encoded.len() - 2); + + let mut decoded_db = MemoryDB::::default(); + let (decoded_root, _) = decode_compact::(&mut decoded_db, &deduplicated).unwrap(); + assert_eq!(decoded_root, root); + + // Node-level deduplication must not change the reconstructed database. + let mut expected_db = MemoryDB::::default(); + decode_compact::(&mut expected_db, &encoded).unwrap(); + assert!(decoded_db == expected_db && expected_db == decoded_db); + + let trie = >::new(&decoded_db, &root).build(); + // The distinct value is present either way. + assert_eq!(trie.get(b"key4").unwrap().as_deref(), Some(&[5u8; 32][..])); + if has_value_nodes::() { + // The shared value node is missing, so lookups must fail with an incomplete + // database error. + match trie.get(b"key1") { + Err(err) => match *err { + TrieError::IncompleteDatabase(_) => {}, + _ => panic!("got unexpected TrieError"), + }, + _ => panic!("lookup was unexpectedly successful"), + } + } else { + // Values are inline; filtering the standalone value node removed nothing. + assert_entries_match::(&decoded_db, root, &entries); + } +} + +#[test] +fn decode_compact_counts_attached_value_items() { + // Layout storing every non-empty value as a separate value node. + type L = reference_trie::HashedValueNoExtThreshold<1>; + + // A single entry: the encoding is exactly the escaped root leaf followed by its detached + // value. The returned item count must include the value item. + let (db, root) = build_trie::(&[(b"key", SHARED_VALUE)]); + let encoded = { + let trie = >::new(&db, &root).build(); + encode_compact::(&trie).unwrap() + }; + assert_eq!(encoded.len(), 2); + assert_eq!(encoded[1].as_slice(), SHARED_VALUE); + + let mut decoded_db = MemoryDB::::default(); + let (decoded_root, used) = decode_compact::(&mut decoded_db, &encoded).unwrap(); + assert_eq!(decoded_root, root); + assert_eq!(used, encoded.len()); +} + +#[test] +fn skip_duplicate_values_across_concatenated_encodings() { + // Layout storing every non-empty value as a separate value node. + type L = reference_trie::HashedValueNoExtThreshold<1>; + + // Two independent tries (think: a top trie and a child trie) sharing a value. + let entries_a: Vec<(&'static [u8], &'static [u8])> = + vec![(b"alpha1", SHARED_VALUE), (b"alpha2", SHARED_VALUE), (b"alpha3", &[7; 32])]; + let entries_b: Vec<(&'static [u8], &'static [u8])> = + vec![(b"beta1", SHARED_VALUE), (b"beta2", SHARED_VALUE), (b"beta9", &[9; 32])]; + let (db_a, root_a) = build_trie::(&entries_a); + let (db_b, root_b) = build_trie::(&entries_b); + + // Thread one seen-set through both encodings: the second references the shared value + // without re-emitting it. + let mut seen_hashes = BTreeSet::new(); + let encoded_a = { + let trie = >::new(&db_a, &root_a).build(); + encode_compact_skip_duplicates::(&trie, &mut seen_hashes).unwrap() + }; + let encoded_b = { + let trie = >::new(&db_b, &root_b).build(); + encode_compact_skip_duplicates::(&trie, &mut seen_hashes).unwrap() + }; + assert_eq!(count_shared_value_items(&encoded_a), 1); + assert_eq!(count_shared_value_items(&encoded_b), 0); + + // Decode both with a threaded known-values map into one prefixed database. + let mut prefixed_db = PrefixedMemoryDB::::default(); + let mut known_items = BTreeMap::new(); + let (decoded_root_a, _) = decode_compact_from_iter_with_known_items::( + &mut prefixed_db, + encoded_a.iter().map(Vec::as_slice), + &mut known_items, + ) + .unwrap(); + let (decoded_root_b, _) = decode_compact_from_iter_with_known_items::( + &mut prefixed_db, + encoded_b.iter().map(Vec::as_slice), + &mut known_items, + ) + .unwrap(); + assert_eq!(decoded_root_a, root_a); + assert_eq!(decoded_root_b, root_b); + assert_entries_match::(&prefixed_db, root_a, &entries_a); + assert_entries_match::(&prefixed_db, root_b, &entries_b); +} + +/// Entries forming two identical subtrees below the root. +/// +/// The mirrored keys diverge at their first nibble and share all following nibbles and values, so +/// the two subtrees below the root branch consist of identically encoded nodes: one branch +/// carrying two leaves (plus value nodes, for layouts detaching values; plus an extension node, +/// for layouts using them). +fn shared_subtree_entries() -> Vec<(&'static [u8], &'static [u8])> { + vec![ + (b"\x00AAAA", SHARED_VALUE), + (b"\x00AAAB", &[6; 32]), + (b"\x10AAAA", SHARED_VALUE), + (b"\x10AAAB", &[6; 32]), + // An unrelated entry so the root is a branch with a third, distinct child. + (b"\xf0ZZZZ", &[7; 32]), + ] +} + +test_layouts!( + skip_duplicates_emits_shared_subtrees_once, + skip_duplicates_emits_shared_subtrees_once_internal +); +fn skip_duplicates_emits_shared_subtrees_once_internal() { + let entries = shared_subtree_entries(); + let (db, root) = build_trie::(&entries); + + let trie = >::new(&db, &root).build(); + let encoded = encode_compact::(&trie).unwrap(); + let deduplicated = encode_compact_skip_duplicates::(&trie, &mut Default::default()).unwrap(); + + // The mirrored subtree (and everything below it) is emitted only once; its second occurrence + // stays a plain hash reference in the root node. This holds for every layout: node-level + // deduplication does not depend on values being stored in separate value nodes. + assert!(deduplicated.len() < encoded.len()); + + // The deduplicated encoding reconstructs a fully readable trie in a hash-keyed database... + let mut hash_keyed_db = MemoryDB::::default(); + let (decoded_root, used) = decode_compact::(&mut hash_keyed_db, &deduplicated).unwrap(); + assert_eq!(decoded_root, root); + assert_eq!(used, deduplicated.len()); + assert_entries_match::(&hash_keyed_db, root, &entries); + + // ...and, via subtree re-insertion, in a position-keyed (prefixed) database as well. + let mut prefixed_db = PrefixedMemoryDB::::default(); + let (decoded_root, _) = decode_compact::(&mut prefixed_db, &deduplicated).unwrap(); + assert_eq!(decoded_root, root); + assert_entries_match::(&prefixed_db, root, &entries); + + // Both encodings reconstruct identical databases: same entries, same reference counts. + let mut expected_hash_keyed_db = MemoryDB::::default(); + decode_compact::(&mut expected_hash_keyed_db, &encoded).unwrap(); + assert!(hash_keyed_db == expected_hash_keyed_db && expected_hash_keyed_db == hash_keyed_db); + let mut expected_prefixed_db = PrefixedMemoryDB::::default(); + decode_compact::(&mut expected_prefixed_db, &encoded).unwrap(); + assert!(prefixed_db == expected_prefixed_db && expected_prefixed_db == prefixed_db); +} + +#[test] +fn skip_duplicates_shares_subtrees_across_concatenated_encodings() { + // Layout storing every non-empty value as a separate value node. + type L = reference_trie::HashedValueNoExtThreshold<1>; + + // Two independent tries containing an identically encoded subtree: in both tries the + // mirrored keys share all nibbles below the diverging first nibble. + let entries_a: Vec<(&'static [u8], &'static [u8])> = + vec![(b"\x00AAAA", SHARED_VALUE), (b"\x00AAAB", &[6; 32]), (b"\xf0ZZZZ", &[7; 32])]; + let entries_b: Vec<(&'static [u8], &'static [u8])> = + vec![(b"\x10AAAA", SHARED_VALUE), (b"\x10AAAB", &[6; 32]), (b"\xe0YYYY", &[8; 32])]; + let (db_a, root_a) = build_trie::(&entries_a); + let (db_b, root_b) = build_trie::(&entries_b); + let trie_a = >::new(&db_a, &root_a).build(); + let trie_b = >::new(&db_b, &root_b).build(); + + // Thread one seen-set through both encodings: the second references the shared subtree + // without re-emitting it. + let mut seen_hashes = BTreeSet::new(); + let encoded_a = encode_compact_skip_duplicates::(&trie_a, &mut seen_hashes).unwrap(); + let encoded_b = encode_compact_skip_duplicates::(&trie_b, &mut seen_hashes).unwrap(); + let standalone_b = + encode_compact_skip_duplicates::(&trie_b, &mut Default::default()).unwrap(); + assert!(encoded_b.len() < standalone_b.len()); + + // Decode both with a threaded known-items map into one prefixed database. + let mut prefixed_db = PrefixedMemoryDB::::default(); + let mut known_items = BTreeMap::new(); + let (decoded_root_a, _) = decode_compact_from_iter_with_known_items::( + &mut prefixed_db, + encoded_a.iter().map(Vec::as_slice), + &mut known_items, + ) + .unwrap(); + let (decoded_root_b, _) = decode_compact_from_iter_with_known_items::( + &mut prefixed_db, + encoded_b.iter().map(Vec::as_slice), + &mut known_items, + ) + .unwrap(); + assert_eq!(decoded_root_a, root_a); + assert_eq!(decoded_root_b, root_b); + assert_entries_match::(&prefixed_db, root_a, &entries_a); + assert_entries_match::(&prefixed_db, root_b, &entries_b); + + // Both encodings prove the shared subtree to the same depth, so this reproduces the plain + // database exactly. In general the cross-encoding reconstruction is only a reference-count + // superset of it (see `decode_compact_from_iter_with_known_items`). + let mut expected_db = PrefixedMemoryDB::::default(); + let plain_a = encode_compact::(&trie_a).unwrap(); + let plain_b = encode_compact::(&trie_b).unwrap(); + decode_compact::(&mut expected_db, &plain_a).unwrap(); + decode_compact::(&mut expected_db, &plain_b).unwrap(); + assert!(prefixed_db == expected_db && expected_db == prefixed_db); +} + +#[test] +fn skip_duplicates_always_emits_the_root() { + // Layout storing every non-empty value as a separate value node. + type L = reference_trie::HashedValueNoExtThreshold<1>; + + let entries = shared_subtree_entries(); + let (db, root) = build_trie::(&entries); + let trie = >::new(&db, &root).build(); + + // Encode the same trie twice with one threaded seen-set. Everything is known when the second + // encoding starts, but the root must still be emitted so the encoding stays individually + // decodable; all its children collapse to plain hash references. + let mut seen_hashes = BTreeSet::new(); + let first = encode_compact_skip_duplicates::(&trie, &mut seen_hashes).unwrap(); + let second = encode_compact_skip_duplicates::(&trie, &mut seen_hashes).unwrap(); + assert!(first.len() > 1); + assert_eq!(second.len(), 1); + + // With a threaded known-items map, decoding the second encoding reconstructs the full + // database below the root, exactly like decoding the first encoding twice would. + let mut prefixed_db = PrefixedMemoryDB::::default(); + let mut known_items = BTreeMap::new(); + let (decoded_root, _) = decode_compact_from_iter_with_known_items::( + &mut prefixed_db, + first.iter().map(Vec::as_slice), + &mut known_items, + ) + .unwrap(); + assert_eq!(decoded_root, root); + let (decoded_root, used) = decode_compact_from_iter_with_known_items::( + &mut prefixed_db, + second.iter().map(Vec::as_slice), + &mut known_items, + ) + .unwrap(); + assert_eq!(decoded_root, root); + assert_eq!(used, 1); + assert_entries_match::(&prefixed_db, root, &entries); + + let mut expected_db = PrefixedMemoryDB::::default(); + decode_compact::(&mut expected_db, &first).unwrap(); + decode_compact::(&mut expected_db, &first).unwrap(); + assert!(prefixed_db == expected_db && expected_db == prefixed_db); +} + #[test] fn encoding_node_owned_and_decoding_node_works() { let entries: Vec<(&[u8], &[u8])> = vec![ @@ -200,3 +593,66 @@ fn encoding_node_owned_and_decoding_node_works() { assert_eq!(record.data, node_owned.to_encoded::<::Codec>()); } } + +#[test] +fn deduplicated_encoding_decodes_with_released_0_31_decoder() { + // Layout storing every non-empty value as a separate value node. + type L = reference_trie::HashedValueNoExtThreshold<1>; + + let entries = shared_value_entries(); + let (db, root) = build_trie::(&entries); + let trie = >::new(&db, &root).build(); + let encoded = encode_compact::(&trie).unwrap(); + let deduplicated = encode_compact_skip_duplicates::(&trie, &mut Default::default()).unwrap(); + assert_eq!(count_shared_value_items(&deduplicated), 1); + + // Baseline: the released decoder handles the unmodified encoding. + let mut baseline_db = MemoryDB::::default(); + let (decoded_root, _) = + reference_trie::trie_db_0_31_decoder::decode_compact_from_iter::( + &mut baseline_db, + encoded.iter().map(Vec::as_slice), + ) + .unwrap(); + assert_eq!(decoded_root, root); + + // The deduplicated encoding decodes with the released decoder into a readable hash-keyed + // database: the value node is present from its first, still-attached occurrence. + // (Prefixed databases are out of scope: 0.31.0 inserts attached values under an incomplete + // prefix, fixed by #227.) + let mut hash_keyed_db = MemoryDB::::default(); + let (decoded_root, _) = + reference_trie::trie_db_0_31_decoder::decode_compact_from_iter::( + &mut hash_keyed_db, + deduplicated.iter().map(Vec::as_slice), + ) + .unwrap(); + assert_eq!(decoded_root, root); + assert_entries_match::(&hash_keyed_db, root, &entries); +} + +#[test] +fn subtree_deduplicated_encoding_decodes_with_released_0_31_decoder() { + // Layout storing every non-empty value as a separate value node. + type L = reference_trie::HashedValueNoExtThreshold<1>; + + let entries = shared_subtree_entries(); + let (db, root) = build_trie::(&entries); + let trie = >::new(&db, &root).build(); + let encoded = encode_compact::(&trie).unwrap(); + let deduplicated = encode_compact_skip_duplicates::(&trie, &mut Default::default()).unwrap(); + assert!(deduplicated.len() < encoded.len()); + + // A deduplicated subtree occurrence is indistinguishable from a node outside the partial + // trie, so the released decoder handles it: the subtree is present in a hash-keyed database + // from its first, still-emitted occurrence. + let mut hash_keyed_db = MemoryDB::::default(); + let (decoded_root, _) = + reference_trie::trie_db_0_31_decoder::decode_compact_from_iter::( + &mut hash_keyed_db, + deduplicated.iter().map(Vec::as_slice), + ) + .unwrap(); + assert_eq!(decoded_root, root); + assert_entries_match::(&hash_keyed_db, root, &entries); +}