Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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