Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions grovedb/src/operations/proof/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1905,6 +1905,13 @@ impl GroveDb {
.query
.has_aggregate_count_and_sum_on_range_anywhere();

// `query.left_to_right` is used verbatim, synthesized levels
// included: this is the definition of the layer's op family, and
// changing it would change proof bytes. The verifier is the side
// that cannot reproduce this value — a subset query does not know
// what the generating query was — so for a synthesized one-key
// level it reads the orientation back off the op family instead.
// See `SinglePathSubquery::synthesized_path_component`.
let mut merk_proof = cost_return_on_error!(
&mut cost,
self.generate_merk_proof(
Expand Down
52 changes: 50 additions & 2 deletions grovedb/src/operations/proof/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1400,17 +1400,65 @@ impl GroveDb {
query
)))?;

// Which direction this layer's op stream is encoded in.
//
// For a real query node the direction is query semantics — it
// decides which end of a range fills a limit — so it comes from
// the query and nothing else. A *synthesized* path-component
// level is different: `query_items_at_path` manufactures it for
// every path component above the query's own path (and for
// positions inside a `subquery_path`), its item list is exactly
// one `QueryItem::Key`, and its `left_to_right` is a fixed
// placeholder. The generating query — which is what chose the
// op family the prover emitted at this path — is not
// recoverable from a subset query, so no fixed value is right:
// a merged descending query emits this layer inverted, and
// running the ascending bound-witness machinery over an
// inverted stream is wrong in both directions (it rejects
// honest proofs, and worse, it can read an absence out of a
// stream that proves presence).
//
// So for a one-key synthesized level, read the orientation off
// the proof's own op family. That is not trusting an
// attacker-chosen parameter: `execute` checks, per op, that
// upright pushes ascend and inverted pushes descend, so a
// stream cannot claim an orientation it does not have — and
// with a single `Key` item the direction cannot reorder,
// truncate or extend the answer either way. Everything that
// binds the result stays where it was: the reconstructed root
// hash still has to match what the parent layer committed, and
// `QueryItem::contains` still gates every returned key.
let single_key_synthesized_level = internal_query.synthesized_path_component
&& matches!(
internal_query.items.as_slice(),
[grovedb_merk::proofs::query::QueryItem::Key(_)]
);
let left_to_right = if single_key_synthesized_level {
grovedb_merk::proofs::query::proof_stream_direction(merk_proof_bytes)
.map_err(|e| {
Error::InvalidProof(
query.clone(),
format!("Invalid V1 proof op stream at path component layer: {}", e),
)
})?
// An op-less stream carries no orientation; `execute`
// rejects it a moment later for having no root.
.unwrap_or(internal_query.left_to_right)
} else {
internal_query.left_to_right
};

let level_query = Query {
items: internal_query.items.to_vec(),
left_to_right: internal_query.left_to_right,
left_to_right,
..Default::default()
};

let (root_hash, merk_result) = level_query
.execute_proof(
merk_proof_bytes,
*limit_left,
internal_query.left_to_right,
left_to_right,
PROOF_VERSION_LATEST, // V1 proof: strict mode rejects items in value hash nodes
)
.unwrap()
Expand Down
54 changes: 51 additions & 3 deletions grovedb/src/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1577,6 +1577,28 @@ pub struct SinglePathSubquery<'a> {
pub left_to_right: bool,
/// In the path of the path_query, or in a subquery path
pub in_path: Option<Cow<'a, Key>>,
/// True when this level was *synthesized* from a path component
/// instead of resolved to a real query node — the `Ordering::Less`
/// arm of [`PathQuery::query_items_at_path`] plus every
/// mid-`subquery_path` arm, all of which go through
/// [`SinglePathSubquery::from_key_when_in_path`].
///
/// A synthesized level's `items` is exactly one `QueryItem::Key`,
/// so its `left_to_right` carries no query semantics at all: the
/// answer is that one key or nothing, and there is no ordering or
/// limit interaction to observe. The field is a placeholder, fixed
/// at `true`, because the direction the *generating* query used at
/// this path — which is what decided the op family the prover
/// emitted — is not recoverable from a subset query.
///
/// Proof verifiers must therefore not take the stream's
/// orientation from `left_to_right` on a synthesized level; they
/// read it off the proof's own op family via
/// `grovedb_merk::proofs::query::proof_stream_direction`, which
/// `execute` independently pins to the stream's key ordering. Proof
/// *generation* keeps using `left_to_right` verbatim, so proof
/// bytes are unaffected.
pub synthesized_path_component: bool,
}

impl fmt::Display for SinglePathSubquery<'_> {
Expand All @@ -1593,6 +1615,11 @@ impl fmt::Display for SinglePathSubquery<'_> {
Some(path) => writeln!(f, " in_path: Some({})", hex_to_ascii(path)),
None => writeln!(f, " in_path: None"),
}?;
writeln!(
f,
" synthesized_path_component: {}",
self.synthesized_path_component
)?;
write!(f, "}}")
}
}
Expand Down Expand Up @@ -1624,8 +1651,14 @@ impl<'a> SinglePathSubquery<'a> {
SinglePathSubquery {
items: Cow::Owned(vec![QueryItem::Key(key.clone())]),
has_subquery: HasSubquery::NoSubquery,
// Placeholder — see `synthesized_path_component`. Nothing
// here knows which direction the generating query walked
// this level in, and for a one-key level nothing needs to:
// the direction is an encoding detail of the proof, which
// is where verifiers read it from.
left_to_right: true,
in_path,
synthesized_path_component: true,
}
}

Expand All @@ -1648,6 +1681,7 @@ impl<'a> SinglePathSubquery<'a> {
has_subquery,
left_to_right: query.left_to_right,
in_path: None,
synthesized_path_component: false,
}
}
}
Expand Down Expand Up @@ -2400,6 +2434,7 @@ mod tests {
has_subquery: HasSubquery::NoSubquery,
left_to_right: true,
in_path: Some(Cow::Borrowed(&root_path_key_2)),
synthesized_path_component: true,
}
);
}
Expand All @@ -2420,6 +2455,7 @@ mod tests {
* subquery for one item */
left_to_right: true,
in_path: None,
synthesized_path_component: false,
}
);
}
Expand All @@ -2442,7 +2478,8 @@ mod tests {
items: Cow::Owned(vec![QueryItem::Key(subquery_path_key_1.clone())]),
has_subquery: HasSubquery::NoSubquery,
left_to_right: true,
in_path: Some(Cow::Borrowed(&subquery_path_key_1))
in_path: Some(Cow::Borrowed(&subquery_path_key_1)),
synthesized_path_component: true,
}
);
}
Expand All @@ -2466,7 +2503,8 @@ mod tests {
items: Cow::Owned(vec![QueryItem::Key(subquery_path_key_2.clone())]),
has_subquery: HasSubquery::NoSubquery,
left_to_right: true,
in_path: Some(Cow::Borrowed(&subquery_path_key_2))
in_path: Some(Cow::Borrowed(&subquery_path_key_2)),
synthesized_path_component: true,
}
);
}
Expand All @@ -2493,6 +2531,7 @@ mod tests {
* add items underneath */
left_to_right: true,
in_path: None,
synthesized_path_component: false,
}
);
}
Expand All @@ -2519,6 +2558,7 @@ mod tests {
has_subquery: HasSubquery::NoSubquery,
left_to_right: true,
in_path: None,
synthesized_path_component: true,
}
);
}
Expand Down Expand Up @@ -2567,6 +2607,7 @@ mod tests {
has_subquery: HasSubquery::Always,
left_to_right: true,
in_path: None,
synthesized_path_component: false,
}
);
}
Expand All @@ -2585,7 +2626,9 @@ mod tests {
items: Cow::Owned(vec![QueryItem::Key(quantum_key.clone())]),
has_subquery: HasSubquery::NoSubquery,
left_to_right: true,
in_path: None, // There should be no path because we are at the end of the path
// There should be no path: we are at the end of the path
in_path: None,
synthesized_path_component: true,
}
);
}
Expand Down Expand Up @@ -2640,6 +2683,7 @@ mod tests {
has_subquery: HasSubquery::NoSubquery,
left_to_right: true,
in_path: Some(Cow::Borrowed(&zero_vec)),
synthesized_path_component: true,
}
);
}
Expand Down Expand Up @@ -2750,6 +2794,7 @@ mod tests {
)),
left_to_right: true,
in_path: None,
synthesized_path_component: false,
}
);
}
Expand All @@ -2768,6 +2813,7 @@ mod tests {
has_subquery: HasSubquery::NoSubquery,
left_to_right: true,
in_path: Some(Cow::Borrowed(&identity_id)),
synthesized_path_component: true,
}
);
}
Expand All @@ -2788,6 +2834,7 @@ mod tests {
)),
left_to_right: true,
in_path: None,
synthesized_path_component: false,
}
);
}
Expand All @@ -2806,6 +2853,7 @@ mod tests {
has_subquery: HasSubquery::NoSubquery,
left_to_right: true,
in_path: None,
synthesized_path_component: false,
}
);
}
Expand Down
Loading
Loading