Storage Pools Architecture
Overview
Storage Pools are BrightChain’s mechanism for logical namespace isolation within the block store. A pool is a lightweight string prefix on block IDs (format: <poolId>:<hash>) that groups blocks together without requiring separate physical storage. Pools enable multi-tenant isolation, per-pool policies, independent lifecycle management, and safe deletion — all while preserving BrightChain’s content-addressable, copy-on-write storage model.
Pools are the foundation for higher-level features: database isolation (BrightDb routes all I/O through a pool), encrypted storage (encryption mode is per-pool), access control (ACLs are per-pool), and cross-node coordination (gossip, reconciliation, and discovery are all pool-scoped).
Why Pools Exist
Without pools, blocks from different databases, tenants, or applications share a single flat namespace. This makes it impossible to:
- Delete all data for a tenant without scanning every block
- Apply per-tenant quotas, retention, or replication policies
- Ensure XOR whitening components stay within a single logical boundary
- Coordinate cross-node replication at the namespace level
Pools solve all of these by introducing a cheap namespace prefix that threads through every layer of the system.
Pool Concepts
PoolId
A PoolId is a string identifier matching /^[a-zA-Z0-9_-]{1,64}$/. Pool IDs are case-sensitive — "Users" and "users" are distinct pools.
import { PoolId, DEFAULT_POOL } from '@brightchain/brightchain-lib';
// Valid pool IDs
const tenantPool: PoolId = 'tenant-alice';
const dbPool: PoolId = 'db_orders';
const tempPool: PoolId = 'temp-session-abc123';
// The default pool for legacy/unpooled blocks
const legacy: PoolId = DEFAULT_POOL; // "default"
Namespace Isolation
Blocks are stored with composite keys: ${poolId}:${hash}. Two blocks with the same content hash but different pool IDs are distinct entries:
Pool A: tenant-alice:a1b2c3d4...
Pool B: tenant-bob:a1b2c3d4...
Operations on Pool A never affect Pool B. hasInPool('tenant-alice', hash) returns true while hasInPool('tenant-bob', hash) returns false, even if the underlying data is identical.
Default Pool
The "default" pool provides backward compatibility. All legacy IBlockStore methods (has, get, put, delete) delegate to the default pool automatically. Existing code continues to work without modification.
Storage Key Round-Trip
The composite key format guarantees a lossless round-trip:
// Construct: poolId + ":" + hash
const key = `${poolId}:${hash}`;
// Parse: split on first ":"
const [parsedPool, parsedHash] = [
key.slice(0, key.indexOf(':')),
key.slice(key.indexOf(':') + 1),
];
// parsedPool === poolId, parsedHash === hash — always
Pool-Scoped Whitening
BrightChain uses the Owner Free Filesystem (OFF) pattern: data is XOR’d with random blocks to produce whitened blocks, and a Constituent Block List (CBL) records the component block IDs needed for reconstruction. Pool-scoped whitening ensures all XOR components live in the same pool.
Why Pool-Scoped Whitening Matters
Without pool scoping, getRandomBlocks() pulls from global storage. A tuple could contain blocks from multiple pools, creating cross-pool XOR dependencies. Deleting Pool A could destroy a random block needed to reconstruct data in Pool B.
Pool-scoped whitening makes each pool a self-contained unit with no external XOR dependencies, enabling safe pool deletion.
How It Works
- Random block sourcing:
getRandomBlocksFromPool(poolId, count)returns only blocks from the specified pool - Tuple creation: All blocks in a tuple (whitened block + random blocks) belong to the same pool
- CBL storage: Both XOR component blocks are stored in the pool via
storeCBLWithWhiteningInPool() - CBL retrieval: Both components are retrieved from the pool via
retrieveCBLFromPool()
graph LR
subgraph "Pool: tenant-alice"
DATA[Source Data]
RB1[Random Block 1<br/>from pool]
RB2[Random Block 2<br/>from pool]
WB[Whitened Block<br/>stored in pool]
CBL[CBL<br/>stored in pool]
end
DATA -->|XOR| WB
RB1 -->|XOR| WB
RB2 -->|XOR| WB
WB -.->|referenced by| CBL
RB1 -.->|referenced by| CBL
RB2 -.->|referenced by| CBL
Pool Bootstrapping
New pools start empty — there are no random blocks for whitening. The bootstrapPool() method seeds a pool with cryptographically random blocks:
// Seed a new pool with 100 random blocks of size 256
await store.bootstrapPool('tenant-alice', BlockSize.Small, 100);
// Now whitening operations have material to work with
const result = await store.storeCBLWithWhiteningInPool('tenant-alice', cblData);
CBL Index Tracking
The CBLIndex (a brightchain-db collection) tracks all whitened CBL storage results with metadata, pool scoping, and user-level organization. Each entry records the magnet URL, both XOR component block IDs, block size, pool ID, creator, visibility, and optional file metadata.
The CBL Index also detects cross-pool dependencies via getCrossPoolDependencies() — if a CBL’s XOR components somehow span multiple pools, neither pool can be safely deleted until the dependency is resolved.
Cross-Node Coordination
BrightChain’s networking layer — gossip, reconciliation, and discovery — is fully pool-aware. Every protocol includes pool context so blocks are replicated, synchronized, and discovered within the correct namespace.
Gossip Protocol
The IGossipService propagates block announcements across nodes. All announcements include the pool ID:
sequenceDiagram
participant Node_A as Node A
participant Gossip as Gossip Service
participant Node_B as Node B
participant Node_C as Node C
Node_A->>Gossip: Block stored in pool "tenant-alice"
Gossip->>Node_B: BlockAnnouncement { poolId: "tenant-alice", type: "add" }
Gossip->>Node_C: BlockAnnouncement { poolId: "tenant-alice", type: "add" }
Node_B->>Node_B: Store in pool "tenant-alice"
Node_C->>Node_C: Store in pool "tenant-alice"
Announcement types include:
add/remove— block added or removed from a poolpool_deleted— entire pool deleted (with TTL-based tombstone propagation)head_update— HeadRegistry pointer updated for a collectioncbl_index_update/cbl_index_delete— CBL Index entry changesacl_update— Pool ACL changes
Reconciliation
The IReconciliationService synchronizes block state after network partition recovery. Manifests are pool-scoped — each manifest maps pool IDs to their block identifier lists:
sequenceDiagram
participant Node_A as Node A
participant Node_B as Node B
Note over Node_A,Node_B: Partition heals
Node_A->>Node_B: Pool-Scoped Manifest<br/>{ "tenant-alice": [block1, block2], "tenant-bob": [block3] }
Node_B->>Node_A: Pool-Scoped Manifest<br/>{ "tenant-alice": [block1, block4], "tenant-bob": [block3] }
Note over Node_A: Missing block4 in tenant-alice
Node_A->>Node_B: Fetch block4 for pool "tenant-alice"
Note over Node_B: Missing block2 in tenant-alice
Node_B->>Node_A: Fetch block2 for pool "tenant-alice"
If a pool has a deletion tombstone locally, reconciliation skips that pool entirely.
Discovery Protocol
The IDiscoveryProtocol locates blocks across the network. Discovery queries accept an optional pool ID and use pool-scoped Bloom filters (keys formatted as poolId:blockId) to avoid false positives from blocks in other pools:
// Discover a block within a specific pool
const locations = await discovery.discoverBlock(blockId, {
poolId: 'tenant-alice',
});
// Search for CBLs by metadata across pool peers
const results = await discovery.searchCBLMetadata({
poolId: 'tenant-alice',
fileName: 'report.pdf',
mimeType: 'application/pdf',
});
Pool Lifecycle
stateDiagram-v2
[*] --> Created: createPool()
Created --> Bootstrapped: bootstrapPool()
Bootstrapped --> Active: store blocks
Active --> Active: read / write / replicate
Active --> PendingDeletion: deletePool()
PendingDeletion --> ValidationCheck: validatePoolDeletion()
ValidationCheck --> Deleted: no cross-pool deps
ValidationCheck --> Active: deps found, deletion rejected
PendingDeletion --> Deleted: forceDeletePool()
Deleted --> Tombstone: gossip pool_deleted
Tombstone --> [*]: TTL expires
Creation
Pool creation is implicit — storing a block in a new pool ID creates the pool. For pools with access control, an ACL is bootstrapped automatically with the creator as sole Admin.
Configuration
Pool configuration includes:
- Encryption mode:
None,NodeSpecific, orPoolShared - Searchable metadata fields: which CBL Index metadata remains unencrypted
- Public access flags:
publicReadandpublicWrite - Replication targets: per-pool replication count
Deletion
Pool deletion follows a safety protocol:
validatePoolDeletion(poolId)scans for cross-pool XOR dependencies- If dependencies exist, deletion is rejected with a list of dependent pools and block IDs
- If no dependencies, all blocks in the pool are removed
- A
pool_deletedgossip announcement propagates to peers with a TTL-based tombstone - While the tombstone exists, new blocks for that pool ID are rejected
forceDeletePool() bypasses the dependency check for administrative use.
Authentication
Node identity in BrightChain is based on ECDSA key pairs. The INodeAuthenticator interface (defined in brightchain-lib, implemented as ECDSANodeAuthenticator in brightchain-api-lib) provides challenge-response authentication:
sequenceDiagram
participant Verifier as Verifying Node
participant Requester as Requesting Node
Requester->>Verifier: Request pool operation
Verifier->>Verifier: createChallenge() → 32 random bytes
Verifier->>Requester: Challenge nonce
Requester->>Requester: signChallenge(nonce, privateKey)
Requester->>Verifier: Signed challenge
Verifier->>Verifier: verifySignature(nonce, signature, publicKey)
alt Signature valid
Verifier->>Requester: Operation proceeds
else Signature invalid
Verifier->>Requester: Rejected + logged
end
Node IDs are derived from public keys via deriveNodeId(publicKey), providing a stable identity tied to the cryptographic key pair. The secp256k1 curve is used, consistent with BrightChain’s existing crypto stack.
Access Control
PoolPermission Enum
Four permission levels control pool access:
| Permission | Grants |
|---|---|
Read | Retrieve blocks from the pool |
Write | Store blocks in the pool |
Replicate | Receive and serve replicas |
Admin | Modify ACL and pool configuration (implies all other permissions) |
IPoolACL
The IPoolACL<TId> interface uses a generic type parameter for DTO flexibility — string for frontend, Uint8Array for backend:
interface IPoolACL<TId = string> {
poolId: PoolId;
owner: TId;
members: IPoolACLMember<TId>[];
publicRead: boolean;
publicWrite: boolean;
previousAclBlockId?: string; // audit chain
approvalSignatures: Array<{ nodeId: TId; signature: Uint8Array }>;
version: number;
updatedAt: Date;
}
ACL Storage
ACLs are stored as signed blocks in the block store itself. This makes them tamper-evident, auditable, and subject to the same replication and durability mechanisms as data blocks. Each ACL update references the previous ACL’s block ID, forming an auditable chain.
Quorum-Based Updates
ACL changes require majority approval from current Admins:
- Multi-admin pools: >50% of Admins must sign the update
- Single-admin pools: the sole Admin’s signature suffices
- Invariant: at least one Admin must always remain — removing the last Admin is rejected
ACL updates are propagated via acl_update gossip announcements to all pool peers.
Enforcement Points
ACL checks are enforced at every layer:
graph TD
OP[Pool Operation] --> BS[Block Store]
OP --> GS[Gossip Service]
OP --> RS[Reconciliation Service]
OP --> DP[Discovery Protocol]
BS -->|Write required| PUT[putInPool]
BS -->|Read required| GET[getFromPool]
BS -->|Admin required| ACL_MOD[Modify ACL]
GS -->|Write or Replicate| ANNOUNCE[Forward announcement]
RS -->|Replicate| RECONCILE[Exchange manifests]
DP -->|Read| DISCOVER[Return results]
The ACLEnforcedBlockStore wrapper intercepts all pool operations and checks permissions before delegating to the underlying store. Permission denied errors include the required permission, actual permissions, and pool ID.
Encrypted Pool Storage
Encryption Modes
Pools support three encryption modes, set at creation time:
| Mode | Behavior | Replication |
|---|---|---|
None | No encryption | Allowed |
NodeSpecific | Encrypted with storing node’s ECIES key; only that node can decrypt | Not allowed |
PoolShared | Encrypted with shared AES-256-GCM key; any pool member can decrypt | Allowed |
Key Management
- Node-specific: encryption key derived from the node’s existing ECDSA key pair
- Pool-shared: a symmetric key is generated and distributed to each member, encrypted with their public key via ECIES
Block IDs are always computed from the ciphertext (not plaintext), so Bloom filters and block lookups work unchanged on encrypted pools.
Key Rotation
Key rotation generates a new key version without re-encrypting existing blocks:
- New symmetric key generated
- New key encrypted per-member via ECIES
- Old key retained in
keyVersionshistory for decrypting older blocks - New blocks use the new key version
Member removal automatically triggers key rotation so the removed member cannot decrypt new blocks.
Searchable Metadata
Pool configuration specifies which CBL Index metadata fields remain unencrypted and searchable. Block IDs always remain unencrypted. Queries on encrypted fields return a descriptive error.
Read Concerns
The ReadConcern enum controls consistency guarantees for cross-node reads:
| Level | Behavior |
|---|---|
Local | Return only locally stored blocks. Fail for remote/unknown blocks. |
Available | Return local blocks immediately. Attempt to fetch remote blocks; return pending indicator if not yet available. |
Consistent | Block until the requested block is available locally or fetched from a remote node, up to the fetch timeout. |
import { ReadConcern } from '@brightchain/brightchain-lib';
// Fast local-only read
const doc = await collection.findOne(query, { readConcern: ReadConcern.Local });
// Wait for cross-node consistency
const doc = await collection.findOne(query, {
readConcern: ReadConcern.Consistent,
});
Architecture Diagrams
Component Relationships
graph TD
subgraph "brightchain-lib (browser-compatible)"
IPS[IPooledBlockStore]
IACL[IPoolACL / PoolPermission]
IAUTH[INodeAuthenticator]
IENC[IEncryptedPoolOptions / EncryptionMode]
ICBL[ICBLIndexEntry]
IRC[ReadConcern]
IGS[IGossipService]
IRS[IReconciliationService]
IDP[IDiscoveryProtocol]
end
subgraph "brightchain-db (Node.js)"
PSA[PooledStoreAdapter]
CBLI[CBLIndex]
HR[HeadRegistry]
COLL[Collection]
end
subgraph "brightchain-api-lib (Node.js)"
ECDSA[ECDSANodeAuthenticator]
PENC[PoolEncryptionService]
ACLS[PoolAclStore]
GOSSIP[GossipService impl]
RECON[ReconciliationService impl]
DISC[DiscoveryProtocol impl]
DISK[DiskBlockAsyncStore]
end
PSA -->|wraps| IPS
PSA -->|scopes to pool| COLL
CBLI -->|uses| COLL
COLL -->|tracks head via| HR
ECDSA -->|implements| IAUTH
PENC -->|uses| IENC
ACLS -->|stores| IACL
ACLS -->|signs with| ECDSA
GOSSIP -->|implements| IGS
RECON -->|implements| IRS
DISC -->|implements| IDP
GOSSIP -->|checks ACL| ACLS
RECON -->|checks ACL| ACLS
DISC -->|checks ACL| ACLS
DISK -->|implements| IPS
Cross-Node Sync Flow
sequenceDiagram
participant App as Application
participant Store as PooledBlockStore
participant Gossip as GossipService
participant PeerA as Peer Node A
participant PeerB as Peer Node B
App->>Store: putInPool("tenant-alice", data)
Store->>Store: Encrypt (if configured)
Store->>Store: Store block
Store->>Gossip: Announce block { poolId, blockId, type: "add" }
Gossip->>PeerA: BlockAnnouncement
Gossip->>PeerB: BlockAnnouncement
Note over PeerA: Verify ACL (Write/Replicate)
PeerA->>PeerA: Store in pool "tenant-alice"
Note over PeerA,PeerB: Later: partition recovery
PeerA->>PeerB: Pool-scoped manifest exchange
PeerB->>PeerA: Missing blocks identified per-pool
PeerA->>PeerB: Sync missing blocks into correct pools
Pool Lifecycle State Machine
stateDiagram-v2
[*] --> Empty: Pool ID first referenced
Empty --> Bootstrapped: bootstrapPool(poolId, blockSize, count)
Bootstrapped --> Active: First data block stored
Empty --> Active: First data block stored (no bootstrap)
Active --> Active: Read / Write / Whitening / Replication
Active --> Validating: deletePool() requested
Validating --> Active: Cross-pool dependencies found
Validating --> Deleting: No dependencies
Active --> Deleting: forceDeletePool()
Deleting --> Tombstoned: Blocks removed, gossip sent
Tombstoned --> [*]: Tombstone TTL expires
Code Examples
Creating a Pool with ACL
import { PooledMemoryBlockStore } from '@brightchain/brightchain-lib';
import { PooledStoreAdapter } from '@brightchain/brightchain-db';
import {
PoolAclStore,
ECDSANodeAuthenticator,
} from '@brightchain/brightchain-api-lib';
import { PoolPermission } from '@brightchain/brightchain-lib';
import { BlockSize } from '@brightchain/brightchain-lib';
// Create the pooled block store
const store = new PooledMemoryBlockStore();
// Bootstrap the pool with random blocks for whitening
await store.bootstrapPool('secure-pool', BlockSize.Small, 50);
// Create an authenticator and generate a key pair
const auth = new ECDSANodeAuthenticator();
const { publicKey, privateKey } = generateKeyPair(); // your ECDSA key pair
const nodeId = auth.deriveNodeId(publicKey);
// Bootstrap the pool ACL — creator becomes sole Admin
const aclStore = new PoolAclStore(store, auth);
const acl = await aclStore.bootstrapPool('secure-pool', nodeId, privateKey, {
publicRead: false,
publicWrite: false,
});
// Add a member with Read + Write permissions (single-admin, no quorum needed)
await aclStore.addMember(
'secure-pool',
{
nodeId: peerNodeId,
permissions: [PoolPermission.Read, PoolPermission.Write],
addedBy: nodeId,
},
privateKey,
);
Storing and Retrieving Data in a Pool
import { PooledMemoryBlockStore } from '@brightchain/brightchain-lib';
import { PooledStoreAdapter } from '@brightchain/brightchain-db';
import { BrightDb } from '@brightchain/brightchain-db';
// Create a pool-scoped adapter
const store = new PooledMemoryBlockStore();
const adapter = new PooledStoreAdapter(store, 'tenant-alice');
// Use with BrightDb — all I/O routes through the pool
const db = new BrightDb({ blockStore: adapter });
const users = db.collection('users');
// Insert a document — stored in pool "tenant-alice"
await users.insert({ name: 'Alice', email: 'alice@example.com' });
// Query — only sees data in pool "tenant-alice"
const result = await users.findOne({ name: 'Alice' });
// Data in other pools is invisible
const otherAdapter = new PooledStoreAdapter(store, 'tenant-bob');
const otherDb = new BrightDb({ blockStore: otherAdapter });
const otherUsers = otherDb.collection('users');
const notFound = await otherUsers.findOne({ name: 'Alice' }); // null
Pool-Scoped CBL Whitening
import { PooledMemoryBlockStore } from '@brightchain/brightchain-lib';
import { PooledStoreAdapter } from '@brightchain/brightchain-db';
import { BlockSize } from '@brightchain/brightchain-lib';
const store = new PooledMemoryBlockStore();
// Bootstrap pool with random blocks
await store.bootstrapPool('my-pool', BlockSize.Small, 50);
// Store a CBL with whitening — both XOR components stay in the pool
const cblData = new Uint8Array([
/* CBL content */
]);
const result = await store.storeCBLWithWhiteningInPool('my-pool', cblData);
// result contains: { blockId1, blockId2, blockSize, magnetUrl }
console.log(result.magnetUrl);
// "magnet:?xt=urn:brightchain:cbl&bs=256&b1=abc123...&b2=def456..."
// Both blocks exist in the pool
console.log(await store.hasInPool('my-pool', result.blockId1)); // true
console.log(await store.hasInPool('my-pool', result.blockId2)); // true
// Retrieve — reconstructs original data from pool-scoped components
const reconstructed = await store.retrieveCBLFromPool(
'my-pool',
result.blockId1,
result.blockId2,
);
// reconstructed === cblData
// Using the adapter, whitening is automatic
const adapter = new PooledStoreAdapter(store, 'my-pool');
const adapterResult = await adapter.storeCBLWithWhitening(cblData);
const adapterReconstructed = await adapter.retrieveCBL(
adapterResult.blockId1,
adapterResult.blockId2,
);
Cross-Node Discovery
import { ReadConcern } from '@brightchain/brightchain-lib';
import type { IDiscoveryProtocol } from '@brightchain/brightchain-lib';
// Discover a block within a specific pool across the network
async function findBlockInPool(
discovery: IDiscoveryProtocol,
blockId: string,
poolId: string,
): Promise<void> {
// Pool-scoped discovery uses pool-prefixed Bloom filters
// to avoid false positives from other pools
const locations = await discovery.discoverBlock(blockId, { poolId });
for (const loc of locations) {
console.log(`Block found on node ${loc.nodeId} in pool ${loc.poolId}`);
}
}
// Search for CBLs by metadata across pool peers
async function searchPoolCBLs(
discovery: IDiscoveryProtocol,
poolId: string,
): Promise<void> {
const results = await discovery.searchCBLMetadata({
poolId,
mimeType: 'application/pdf',
tags: ['reports'],
});
for (const entry of results) {
console.log(`Found: ${entry.metadata?.fileName} — ${entry.magnetUrl}`);
}
}
Related Specs
This document synthesizes concepts from four detailed specs:
| Spec | Covers |
|---|---|
| Pool-Based Storage Isolation | Core pool primitives: IPooledBlockStore, PooledMemoryBlockStore, PooledStoreAdapter, storage keys, pool stats, deletion, backward compatibility |
| Pool-Scoped Whitening | Pool-scoped random block sourcing, tuple creation, BlockHandleTuple pool validation, pool bootstrapping, safe deletion with cross-pool dependency checks, CBL pool awareness |
| Cross-Node Eventual Consistency | Block fetching protocol, ReadConcern levels, query result enrichment, fetch queue deduplication, pool-scoped remote fetching, gossip-triggered proactive fetching |
| Cross-Node Pool Coordination | Pool-aware block announcements, pool deletion gossip, pool-scoped reconciliation, pool-scoped discovery with Bloom filters, pool metadata in location records, pool-aware replication tracking |
Each spec has its own requirements, design, and task documents in .kiro/specs/.