How to Prove One Record Belongs to a Dataset
Suppose a recipient trusts a catalogue snapshot but needs to verify only one record from it. Sending the whole catalogue for every check is unnecessary. A Merkle proof supplies the sibling hashes needed to reconstruct the root from the claimed record's leaf, position and tree size. If the reconstructed root matches the trusted root, the record belongs to that committed snapshot under the chosen hashing and encoding rules.
Sphere10 Framework's stream-mapped Merkle list combines serialized record storage with an integrity tree. We will create a four-order dataset, prove the second record and run two negative checks: one changes the serialized payload, and the other claims a different position. After generating the proof, the example verifies the original and rejects both altered claims. This provides a practical bridge from blockchain Merkle-tree concepts to ordinary application data.
Contents
Prepare the Example
Use the .NET 10 SDK and Sphere10 Framework 3.1.3. In a new console project, add the core package:
dotnet new console --framework net10.0 --name RecordInclusionProof
cd RecordInclusionProof
dotnet add package Sphere10.Framework --version 3.1.3
Replace the generated source with the named files below, or download the complete example at the end. The package supplies the storage, serialization and collection mechanisms, while the small model and workflow belong to this application. This example uses an isolated in-memory dataset, so running it does not alter an existing application store.
Agree on the Encoded Leaf
A proof verifier must hash the same bytes as the producer. The visible text ORDER-102|75.00 is the application's record representation, but the stream-mapped list stores it through a string serializer. Its serialized framing is part of the leaf encoding. Hashing a display label with a different encoding or omitting serializer framing would ask a different question of the proof.
For that reason, this example obtains the record's serialized bytes through ObjectStream.GetItemBytes and applies the same null-support hashing contract used by the list's item hasher. In a real producer/verifier exchange, transmit those canonical bytes or define an identical, versioned serialization contract on both sides. Also retain the hash algorithm, tree size, position and proof order.
Generate and Verify the Proof
Save the following complete example as Program.cs:
using System;
using System.IO;
using System.Linq;
using Sphere10.Framework;
namespace ArticleSample;
public static class Program {
public static void Main() {
const int recordIndex = 1;
const CHF algorithm = CHF.SHA2_256;
using var raw = new MemoryStream();
using var records = new StreamMappedMerkleList<string>(raw, algorithm, 256, itemSerializer: StringSerializer.UTF8, autoLoad: true);
records.AddRange(new[] { "ORDER-101|125.00", "ORDER-102|75.00", "ORDER-103|49.00", "ORDER-104|200.00" });
var trustedRoot = (byte[])records.MerkleTree.Root.Clone();
var snapshotSize = records.MerkleTree.Size;
var proof = records.MerkleTree.GenerateExistenceProof(recordIndex).ToArray();
var payload = records.ObjectStream.GetItemBytes(recordIndex);
var leaf = Hashers.HashWithNullSupport(algorithm, payload);
var valid = MerkleMath.VerifyExistenceProof(algorithm, trustedRoot, snapshotSize, recordIndex, leaf, proof);
var modifiedPayload = (byte[])payload.Clone();
modifiedPayload[^1] ^= 1;
var modifiedLeaf = Hashers.HashWithNullSupport(algorithm, modifiedPayload);
var modifiedAccepted = MerkleMath.VerifyExistenceProof(algorithm, trustedRoot, snapshotSize, recordIndex, modifiedLeaf, proof);
var wrongPositionAccepted = MerkleMath.VerifyExistenceProof(algorithm, trustedRoot, snapshotSize, 0, leaf, proof);
if (!valid || modifiedAccepted || wrongPositionAccepted)
throw new InvalidOperationException("Proof validation did not match the expected outcomes.");
Console.WriteLine($"Dataset records: {snapshotSize.LeafCount}");
Console.WriteLine($"Proof sibling hashes: {proof.Length}");
Console.WriteLine($"Original payload accepted: {valid}");
Console.WriteLine($"Modified payload accepted: {modifiedAccepted}");
Console.WriteLine($"Wrong position accepted: {wrongPositionAccepted}");
}
}
The trusted root is copied before the checks, making its role as a snapshot checkpoint explicit. GenerateExistenceProof returns the sibling hashes along the relevant path. VerifyExistenceProof combines those hashes with the claimed leaf and position according to the tree's rules.
The modified-payload check flips one byte in a copy, leaving the original stored record unchanged. It then hashes the modified bytes and presents the original proof. The wrong-position check retains the genuine payload hash but claims position zero instead of position one. Both should fail. The code throws if the valid proof is rejected or either invalid claim is accepted.
Inspect the Outcomes
dotnet run -c Release
The expected output is:
Dataset records: 4
Proof sibling hashes: 2
Original payload accepted: True
Modified payload accepted: False
Wrong position accepted: False
These checks are intentionally observable. The valid result proves that generation and verification agree for the selected serialized record. The two false results demonstrate that the proof is bound to both content and position. They do not claim that this tiny example is a full security audit of an application protocol or every possible malformed-proof input.
Use a Snapshot, Not an Ambiguous Moving Target
A dataset can change after its root is captured. Keep the proof's tree size and root together with the version of the dataset it describes. A proof for an older snapshot does not become a promise about the current state merely because the same record value still appears somewhere in the collection.
The tree machinery also supports more than static examples, but different proof types answer different questions. Existence, update and consistency proofs should not be conflated. Make the claim precise by stating that this serialized record occupied this position in the dataset committed by this root. Add another proof family only when the application actually needs that stronger or different statement.
Useful applications include verifiable exports, independently checked catalogue records and integrity proofs for selected data. Whether the data is a blockchain transaction or an ordinary document record, the surrounding application still owns access control, issuer authentication, retention and business interpretation. Merkle proofs solve the membership problem, not all those adjacent problems at once.
Continue Building
The complete sample project includes the project file, source, expected console output and MIT license notice. Code excerpts on this page are supplied under that same license. Keep the package version with your application and retain the sample's observable checks when adapting it because compilation alone does not prove persistence or correct query results.
Sphere10 Framework lets you compose this capability with the rest of your application rather than adopt an unrelated application shell. Start with the working example, change one schema or workload decision at a time, and verify the resulting behavior with your own records.
Read the ObjectSpace integrity article for a whole-store checkpoint, and the existing Sphere 10 Dynamic Merkle-Trees material for the underlying tree model and proof families.
Useful references:
-
Stream-mapped Merkle list
-
Merkle tree and proof APIs
-
Sphere10 Framework on NuGet
-
Explore Sphere10 Framework
Continue the Series
How to Add Cryptographic Integrity to an Object Store
How to Store Many Independent Streams Inside One File