Federated AEF Anchoring
Federated anchoring lets multiple customers cross-anchor evidence under one shared Merkle root, then prove that their own record existed at time T without revealing any other tenant's content.
Threat model
- Every participant keeps its own
record_hash. - The shared manifest stores a blinded
commitment_hash, not raw evidence. - The Merkle root is the only value sent to OpenTimestamps calendars.
- Every participant receives a proof path that resolves only their commitment into the shared root.
This gives each tenant a public timestamp proof without exposing sibling records or the full tenant population.
Manifest shape
Each manifest member contains:
participant_idrecord_hashscopenoncecommitment_hashproof
The commitment is deterministic:
{
"nonce": "...",
"participant_id": "...",
"record_hash": "...",
"scope": "..."
}
That JSON object is canonicalized with sorted keys, compact separators, and ensure_ascii=false, then hashed with SHA-256.
CLI flow
axiom federated-build --input ./members.json --output ./federated-manifest.json
axiom federated-anchor --manifest ./federated-manifest.json --output ./anchored-manifest.json
axiom federated-verify \
--manifest ./anchored-manifest.json \
--participant-id tenant-alpha \
--record-hash aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
--scope prod
Python flow
from axiom_sdk import (
anchor_federated_manifest,
build_federated_anchor_manifest,
lookup_federated_anchor_member,
verify_federated_anchor_member,
)
manifest = build_federated_anchor_manifest(
[
{"participant_id": "tenant-alpha", "record_hash": "a" * 64, "scope": "prod"},
{"participant_id": "tenant-bravo", "record_hash": "b" * 64, "scope": "prod"},
]
)
anchored_manifest = anchor_federated_manifest(manifest)
member = lookup_federated_anchor_member(
anchored_manifest,
participant_id="tenant-alpha",
record_hash="a" * 64,
scope="prod",
)
result = verify_federated_anchor_member(member, merkle_root=anchored_manifest.merkle_root)
assert result.ok is True
Operational constraints
record_hashmust already be a 64-character lowercase SHA-256 hex digest.noncemust be a non-trivial even-length hex string. If omitted, the SDK generates one.- Duplicate
(participant_id, record_hash, scope, nonce)entries are rejected. - The current implementation anchors the shared Merkle root through OpenTimestamps. It does not yet run a hosted federation coordinator.