Skip to content

[storage/qmdb/current] have current proof types implement codec#3717

Merged
patrick-ogrady merged 10 commits into
mainfrom
codec-proofs
May 5, 2026
Merged

[storage/qmdb/current] have current proof types implement codec#3717
patrick-ogrady merged 10 commits into
mainfrom
codec-proofs

Conversation

@roberto-bayardo

Copy link
Copy Markdown
Collaborator

Resolves: #3705

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 5, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
commonware-mcp 19f1c7d May 05 2026, 11:14 PM

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Benchmark results

Tip

PASSED: No benchmark exceeded the regression threshold.

Benchmark comparison table
Benchmark Baseline (main) Current Delta Threshold Status
qmdb::merkleize/variant=any::unordered::fixed::mmr keys=10000 ch=false sync=false 1.731 ms 1.562 ms -9.80% 10.00% ✅ PASS
qmdb::merkleize/variant=current::ordered::fixed::mmb chunk=256 keys=10000 ch=true sync=false 2.973 ms 2.895 ms -2.65% 10.00% ✅ PASS

Baseline commit(s): dec68db8757a

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds commonware-codec serialization support to QMDB “current” proof structures so they can be shipped over the wire and round-tripped in tests.

Changes:

  • Implement Write, EncodeSize, and Read for RangeProof<F, D> with bounded decoding via ReadRangeExt.
  • Implement Write, EncodeSize, and Read for OperationProof<F, D, N> (threading config to the embedded RangeProof).
  • Add unit tests that round-trip encode/decode for both proof types.

Comment thread storage/src/qmdb/current/proof.rs Outdated
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 5, 2026

Copy link
Copy Markdown

Deploying monorepo with  Cloudflare Pages  Cloudflare Pages

Latest commit: 19f1c7d
Status: ✅  Deploy successful!
Preview URL: https://22c6cefa.monorepo-eu0.pages.dev
Branch Preview URL: https://codec-proofs.monorepo-eu0.pages.dev

View logs

@roberto-bayardo roberto-bayardo moved this to In Progress in Tracker May 5, 2026
@roberto-bayardo roberto-bayardo marked this pull request as ready for review May 5, 2026 19:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@patrick-ogrady patrick-ogrady left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should better leverage codec

Comment thread storage/src/qmdb/current/ordered/mod.rs Outdated
Self::KeyValue(op_proof, update) => {
EXCLUSION_TAG_KEY_VALUE.write(buf);
op_proof.write(buf);
update.key.write(buf);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we don’t define write on update?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI slop, fixed.

Comment thread storage/src/qmdb/current/ordered/mod.rs Outdated
Self::Commit(op_proof, value) => {
EXCLUSION_TAG_COMMIT.write(buf);
op_proof.write(buf);
value.is_some().write(buf);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option write is already supported by Codec!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread storage/src/qmdb/current/ordered/mod.rs Outdated
}
EXCLUSION_TAG_COMMIT => {
let op_proof = OperationProof::<F, D, N>::read_cfg(buf, max_digests)?;
let value = if bool::read(buf)? {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment about option support

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread storage/src/qmdb/current/proof.rs Outdated
}
self.unfolded_prefix_peaks.write(buf);
self.partial_chunk_digest.is_some().write(buf);
if let Some(digest) = &self.partial_chunk_digest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment about option support

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@patrick-ogrady

Copy link
Copy Markdown
Contributor

Another finding:

path: storage/src/qmdb/current/proof.rs
line: 748
severity: medium
---
`OperationProof` is now decodable from untrusted bytes, but `verify` indexes the decoded bitmap chunk before enforcing the chunk-size invariant that `current::init` enforces for real DBs. A decoded `OperationProof<_, _, 0>` reaches `verify` and panics in `BitMap::<0>::get_bit_from_chunk` instead of returning `false`. Please reject `N == 0` before bitmap indexing and before the delegated `RangeProof::verify` path can divide by `CHUNK_SIZE_BITS`.

Test command: `just test -p commonware-storage test_decoded_operation_proof_zero_sized_chunk_does_not_panic`

Observed result: the test fails with a panic at `utils/src/bitmap/mod.rs:535`: `attempt to calculate the remainder with a divisor of zero`.

<details><summary>Regression test patch</summary>

```patch
diff --git a/storage/src/qmdb/current/proof.rs b/storage/src/qmdb/current/proof.rs
index d49ea1d80..ce5ac8b73 100644
--- a/storage/src/qmdb/current/proof.rs
+++ b/storage/src/qmdb/current/proof.rs
@@ -913,6 +913,40 @@ mod tests {
         assert_eq!(decoded, proof);
     }
 
+    #[test]
+    fn test_decoded_operation_proof_zero_sized_chunk_does_not_panic() {
+        type F = mmb::Family;
+        const N: usize = 0;
+        const MAX_DIGESTS: usize = 64;
+
+        let proof = OperationProof::<F, sha256::Digest, N> {
+            loc: mmb::Location::new(0),
+            chunk: [],
+            range_proof: RangeProof {
+                proof: Proof::<F, sha256::Digest> {
+                    leaves: mmb::Location::new(0),
+                    inactive_peaks: 0,
+                    digests: vec![],
+                },
+                pre_prefix_acc: None,
+                unfolded_prefix_peaks: vec![],
+                partial_chunk_digest: None,
+                ops_root: Sha256::hash(b"ops"),
+            },
+        };
+        let decoded =
+            OperationProof::<F, sha256::Digest, N>::decode_cfg(proof.encode(), &MAX_DIGESTS)
+                .unwrap();
+        let root = Sha256::hash(b"root");
+
+        let result = std::panic::catch_unwind(|| {
+            let mut hasher = Sha256::new();
+            decoded.verify(&mut hasher, 7u8, &root)
+        });
+        assert!(result.is_ok(), "verify panicked for zero-sized bitmap chunks");
+        assert!(!result.unwrap(), "zero-sized bitmap chunk proof verified");
+    }
+
     #[test_traced]
     fn test_range_proof_verifies_for_mmb_multi_peak_chunk() {
         let executor = deterministic::Runner::default();

One unique actionable finding remained after deduplication: decoded OperationProof values with zero-sized bitmap chunks can panic during verification instead of failing cleanly.

@patrick-ogrady

Copy link
Copy Markdown
Contributor

Another finding:

path: storage/src/qmdb/current/proof.rs
line: 748
severity: medium
---
`OperationProof` is now decodable from untrusted bytes, but `verify` indexes the decoded bitmap chunk before enforcing the chunk-size invariant that `current::init` enforces for real DBs. A decoded `OperationProof<_, _, 0>` reaches `verify` and panics in `BitMap::<0>::get_bit_from_chunk` instead of returning `false`. Please reject `N == 0` before bitmap indexing and before the delegated `RangeProof::verify` path can divide by `CHUNK_SIZE_BITS`.

Test command: `just test -p commonware-storage test_decoded_operation_proof_zero_sized_chunk_does_not_panic`

Observed result: the test fails with a panic at `utils/src/bitmap/mod.rs:535`: `attempt to calculate the remainder with a divisor of zero`.

<details><summary>Regression test patch</summary>

```patch
diff --git a/storage/src/qmdb/current/proof.rs b/storage/src/qmdb/current/proof.rs
index d49ea1d80..ce5ac8b73 100644
--- a/storage/src/qmdb/current/proof.rs
+++ b/storage/src/qmdb/current/proof.rs
@@ -913,6 +913,40 @@ mod tests {
         assert_eq!(decoded, proof);
     }
 
+    #[test]
+    fn test_decoded_operation_proof_zero_sized_chunk_does_not_panic() {
+        type F = mmb::Family;
+        const N: usize = 0;
+        const MAX_DIGESTS: usize = 64;
+
+        let proof = OperationProof::<F, sha256::Digest, N> {
+            loc: mmb::Location::new(0),
+            chunk: [],
+            range_proof: RangeProof {
+                proof: Proof::<F, sha256::Digest> {
+                    leaves: mmb::Location::new(0),
+                    inactive_peaks: 0,
+                    digests: vec![],
+                },
+                pre_prefix_acc: None,
+                unfolded_prefix_peaks: vec![],
+                partial_chunk_digest: None,
+                ops_root: Sha256::hash(b"ops"),
+            },
+        };
+        let decoded =
+            OperationProof::<F, sha256::Digest, N>::decode_cfg(proof.encode(), &MAX_DIGESTS)
+                .unwrap();
+        let root = Sha256::hash(b"root");
+
+        let result = std::panic::catch_unwind(|| {
+            let mut hasher = Sha256::new();
+            decoded.verify(&mut hasher, 7u8, &root)
+        });
+        assert!(result.is_ok(), "verify panicked for zero-sized bitmap chunks");
+        assert!(!result.unwrap(), "zero-sized bitmap chunk proof verified");
+    }
+
     #[test_traced]
     fn test_range_proof_verifies_for_mmb_multi_peak_chunk() {
         let executor = deterministic::Runner::default();

One unique actionable finding remained after deduplication: decoded OperationProof values with zero-sized bitmap chunks can panic during verification instead of failing cleanly.

Ah, this is dumb. N is controlled.

Comment thread storage/src/qmdb/current/ordered/mod.rs Outdated
}

#[cfg(test)]
mod codec_tests {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can just be part of tests?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes done

Comment thread storage/src/qmdb/current/ordered/mod.rs Outdated
let value = Option::<V::Value>::read_cfg(buf, value_cfg)?;
Ok(Self::Commit(op_proof, value))
}
_ => Err(commonware_codec::Error::Invalid(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is an "InvalidEnum"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@patrick-ogrady

Copy link
Copy Markdown
Contributor

We should add conformance tests for these new proof types.

@patrick-ogrady

Copy link
Copy Markdown
Contributor

max_digests is currently enforced three times, not once: Proof::read_cfg caps proof.digests, then RangeProof::read_cfg separately allows up to max_digests more entries in both unfolded_prefix_peaks and unfolded_suffix_peaks. Because OperationProof::read_cfg, KeyValueProof::read_cfg, and ExclusionProof::read_cfg all forward the same cfg unchanged, every new decoder added in this PR can accept a well-formed payload whose total digest allocation is roughly 3 * max_digests. That defeats the caller's allocation budget and reintroduces the DoS vector this cfg is supposed to prevent. Please enforce a single remaining digest budget across all three vectors, or change the cfg shape to explicit per-field budgets and document that contract.

@roberto-bayardo

Copy link
Copy Markdown
Collaborator Author

max_digests is currently enforced three times, not once: Proof::read_cfg caps proof.digests, then RangeProof::read_cfg separately allows up to max_digests more entries in both unfolded_prefix_peaks and unfolded_suffix_peaks. Because OperationProof::read_cfg, KeyValueProof::read_cfg, and ExclusionProof::read_cfg all forward the same cfg unchanged, every new decoder added in this PR can accept a well-formed payload whose total digest allocation is roughly 3 * max_digests. That defeats the caller's allocation budget and reintroduces the DoS vector this cfg is supposed to prevent. Please enforce a single remaining digest budget across all three vectors, or change the cfg shape to explicit per-field budgets and document that contract.

This is fine since max-digests is generally around 64 -- hard to DoS with that few digests even if it is *N. I can document better.

@roberto-bayardo

Copy link
Copy Markdown
Collaborator Author

We should add conformance tests for these new proof types.

done

@patrick-ogrady

Copy link
Copy Markdown
Contributor

This is fine since max-digests is generally around 64 -- hard to DoS with that few digests even if it is *N. I can document better.

What do we do in other proof decode functions?

@roberto-bayardo

Copy link
Copy Markdown
Collaborator Author

This is fine since max-digests is generally around 64 -- hard to DoS with that few digests even if it is *N. I can document better.

What do we do in other proof decode functions?

The other proof decoder only had one field of digests.

Comment thread storage/conformance.toml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

Comment thread storage/src/qmdb/current/proof.rs
@patrick-ogrady

Copy link
Copy Markdown
Contributor

Should fully unblock exoware!

@patrick-ogrady patrick-ogrady merged commit b55a48c into main May 5, 2026
174 of 175 checks passed
@patrick-ogrady patrick-ogrady deleted the codec-proofs branch May 5, 2026 23:29
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Tracker May 5, 2026
@codecov

codecov Bot commented May 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.83%. Comparing base (dec68db) to head (19f1c7d).
⚠️ Report is 1 commits behind head on main.

@@           Coverage Diff            @@
##             main    #3717    +/-   ##
========================================
  Coverage   95.83%   95.83%            
========================================
  Files         458      458            
  Lines      180650   180964   +314     
  Branches     4218     4218            
========================================
+ Hits       173118   173430   +312     
- Misses       6186     6187     +1     
- Partials     1346     1347     +1     
Files with missing lines Coverage Δ
storage/src/qmdb/current/ordered/db.rs 87.50% <100.00%> (+1.93%) ⬆️
storage/src/qmdb/current/ordered/mod.rs 99.85% <100.00%> (+0.03%) ⬆️
storage/src/qmdb/current/proof.rs 96.47% <100.00%> (+0.48%) ⬆️

... and 3 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update dec68db...19f1c7d. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

[storage/qmdb] Add codec impls to proof types (RangeProof, OperationProof, KeyValueProof, ExclusionProof)

3 participants