How to Add Cryptographic Integrity to an Object Store
A stored value can be readable and still be the wrong value. A catalogue may have changed since a review, a data snapshot may differ from its accepted checkpoint, or a transfer may need a way to identify the exact dataset it represents. A cryptographic integrity root gives an application a compact commitment to the stored content, provided the expected root is obtained from a trustworthy place.
Sphere10 Framework can maintain Merkle attachments for an ObjectSpace's dimensions and combine their roots into a root for the space. We will save a product, flush the store, capture its root, change the price and capture the next root. The example then checks internal tree/header consistency and shows that the new data does not match the earlier checkpoint. Those checks are related but different because a consistent current store can legitimately differ from an earlier accepted version.
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 MerkleObjectSpace
cd MerkleObjectSpace
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.
Define the Record Being Committed
Use Product.cs for the persisted model:
namespace ArticleSample;
public sealed class Product {
public long Id { get; set; }
public string Sku { get; set; }
public string Name { get; set; }
public int Category { get; set; }
public decimal Price { get; set; }
}
A Merkle commitment depends on the encoded record, its position and the configured tree/hash rules. Keep the schema, serializers, byte order and hash algorithm with the data contract. Comparing roots generated from different encodings is not a reliable way to compare business objects that merely look similar on screen.
This example uses SHA-256 through Framework's Hashers infrastructure and enables Merkleization before building the store. It does not use the reference concatenation hash shown in some tree-learning tests. Real integrity checks need a suitable registered cryptographic hash, not a convenient debugging stand-in.
Expose a Focused Application Checkpoint API
The core maintains the storage mechanism. Our application adds the small VerifiableObjectSpace extension below to expose the root/header mapping for the selected storage version and a consistency operation. The ReadRoot method below is an application-owned wrapper around the protected stream extension point.
Save it as VerifiableObjectSpace.cs:
using Sphere10.Framework;
using Sphere10.Framework.ObjectSpaces;
namespace ArticleSample;
public sealed class VerifiableObjectSpace : MemoryObjectSpace {
private VerifiableObjectSpace(ObjectSpaceDefinition definition, SerializerFactory serializers, ComparerFactory comparers)
: base(definition, serializers, comparers) {
}
public static VerifiableObjectSpace Create() {
var serializers = new SerializerFactory(SerializerFactory.Default);
serializers.RegisterAutoBuild<Product>();
var builder = new ObjectSpaceBuilder().UseMemoryStream().UsingHashFunction(CHF.SHA2_256);
builder.AddDimension<Product>(ignoreAnnotations: true).WithIdentifier(item => item.Id).Done();
builder.Merkleized();
return new VerifiableObjectSpace(builder.BuildDefinition(), serializers, new ComparerFactory(ComparerFactory.Default));
}
public byte[] ReadRoot() {
using (EnterAccessScope()) {
var length = Hashers.GetDigestSizeBytes(Definition.HashFunction);
// This application-owned extension targets the pinned ObjectSpace header layout.
return (byte[])_streams.Header.MapExtensionProperty(0, sizeof(bool) + length, new ConstantSizeNullableByteArraySerializer(length)).Value.Clone();
}
}
public void VerifyStorageConsistency() {
_streams.Attachments[Sphere10FrameworkDefaults.DefaultSpatialMerkleTreeIndexName].VerifyIntegrity();
}
}
The helper creates its own serializer and comparer factories rather than mutating global registration state. Its header mapping is deliberately tied to the selected ObjectSpace format, in which the nullable root occupies a null marker plus the algorithm's digest bytes. It returns a cloned root, so the application's checkpoint does not alias the mapped value.
Capture Two States and Compare Them
Save this as Program.cs:
using System;
using Sphere10.Framework;
using Sphere10.Framework.ObjectSpaces;
namespace ArticleSample;
public static class Program {
public static void Main() {
using var space = VerifiableObjectSpace.Create();
var product = new Product { Id = 101, Sku = "KB-101", Name = "Keyboard", Category = 7, Price = 89m };
space.Save(product);
space.Flush();
var original = space.ReadRoot();
product.Price = 95m;
space.Save(product);
space.Flush();
var changed = space.ReadRoot();
var rootChanged = !ByteArrayEqualityComparer.Instance.Equals(original, changed);
space.VerifyStorageConsistency();
var matchesOldCheckpoint = ByteArrayEqualityComparer.Instance.Equals(original, changed);
if (!rootChanged || matchesOldCheckpoint)
throw new InvalidOperationException("Merkle integrity demonstration failed.");
Console.WriteLine($"Root changed after Save and Flush: {rootChanged}");
Console.WriteLine("Consistent tree/header: passed");
Console.WriteLine($"Matches earlier trusted checkpoint: {matchesOldCheckpoint}");
}
}
Run the sample:
dotnet run -c Release
The expected output is:
Root changed after Save and Flush: True
Consistent tree/header: passed
Matches earlier trusted checkpoint: False
The first root is captured only after Save and Flush. The product is then changed, saved and flushed again before the second root is read. That sequencing matters because Merkle calculation and publication of roots can be deferred until the store flushes. The test compares actual byte content using Framework's byte-array comparer.
The current tree/header passes the attachment's consistency check, while its root differs from the earlier checkpoint. The example therefore reports both facts instead of interpreting every root difference as corruption. In an import approval flow, the application could record which root was accepted and require later consumers to verify that exact snapshot.
Separate Consistency, Membership and Authenticity
The attachment check shown here verifies its implemented storage/tree relationships. It is not advertised as a complete rehash of every arbitrary byte in a damaged file, an append-only audit log or a digital signature. A complete validation design must specify which serialized records are hashed, which root is trusted and how that root is retained or authenticated.
Membership is another useful operation when a recipient needs to verify one record against a trusted root without receiving the entire collection. That is the subject of the inclusion-proof tutorial. Authenticity is separate again. A signature can bind a trusted issuer to a checkpoint or manifest, while the Merkle proof establishes the included data's relationship to that checkpoint.
Use these primitives where their contracts fit the application. Keep integrity checkpoints versioned with their hash and encoding rules, and verify the committed file or transfer artifact rather than only an object still held in memory.
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.
The inclusion-proof article takes the next step by showing how to transmit one serialized record and its sibling hashes, then reject a modified record or a different claimed position.
Useful references:
-
ObjectSpace Merkle configuration
-
Dynamic Merkle tree implementation
-
Sphere10 Framework on NuGet
-
Explore Sphere10 Framework
Continue the Series
How to Build a Self-Describing Object Store
How to Prove One Record Belongs to a Dataset