1. Version Type System

  enum Version {
      Txn(u64),        ← Transaction-level MVCC version (global monotonic)
      Sequence(u64),   ← Append-only position in event log (per-branch monotonic)
      Counter(u64),    ← Per-entity mutation count (per-cell/doc/record)
  }

Location: crates/core/src/contract/version.rs

Key Properties

MethodBehavior
as_u64()Returns raw u64 — strips variant tag
PartialEq / EqFull enum comparison — Counter(5) != Txn(5)
OrdDiscriminant first (Txn < Sequence < Counter), then value
increment()Preserves variant — Counter(5).increment() → Counter(6)
From<u64>Creates Version::Txn(v) — defaults to Txn

2. Two-Level Versioning Architecture

  STORAGE LAYER (MVCC)                   VALUE LAYER (Primitive)
  ────────────────────                   ───────────────────────

  VersionChain for key "state:my-cell"
  ┌─────────────────────────────────┐
  │ StoredValue                     │
  │   version: Version::Txn(42)    │ ← Outer: MVCC snapshot version
  │   value: Value::String(         │
  │     '{"value": 7,              │
  │      "version": Counter(3),    │ ← Inner: Per-cell mutation count
  │      "updated_at": 1234}'      │
  │   )                            │
  ├─────────────────────────────────┤
  │ StoredValue                     │
  │   version: Version::Txn(38)    │ ← Outer: from earlier transaction
  │   value: Value::String(         │
  │     '{"value": 5,              │
  │      "version": Counter(2),    │ ← Inner: earlier mutation count
  │      "updated_at": 1230}'      │
  │   )                            │
  └─────────────────────────────────┘

The outer version (Txn) is used for:

  • MVCC snapshot reads (get_at_version(max_version))
  • Transaction conflict detection (read-set validation)
  • Global ordering of commits

The inner version (Counter/Sequence) is used for:

  • Client-facing version numbers (CAS expected_counter)
  • Primitive-specific semantics (event ordering, state mutation count)

3. Version Usage by Primitive

PrimitiveInner VersionSourceCounter SpaceWhere Created
KVTxnTransaction commit versionGlobal AtomicU64kv.rs:122
EventSequencePer-branch EventLogMeta.next_sequencePer-branch monotonicevent.rs:378
StateCounterPer-cell counter in State structPer-cell, starts at 1state.rs:125,218,244
JSONCounterPer-document counter in JsonDoc structPer-document, starts at 1json.rs:267,395,448
VectorCounterPer-record version in VectorRecord structPer-recordstore.rs:459
BranchCounterPer-branch metadata counterPer-branchindex.rs:149

KV is the exception

KV is the only primitive where the inner version == the outer version. KV returns Version::Txn(commit_version) directly from kv_put(). All other primitives maintain their own version counters independent of the MVCC transaction version.

4. Storage Layer Invariant

// crates/storage/src/sharded.rs:83-88
pub fn get_at_version(&self, max_version: u64) -> Option<&StoredValue> {
    debug_assert!(
        self.versions.iter().all(|sv| sv.version().is_txn()),
        "Storage layer should only contain Txn versions"
    );
    self.versions.iter().find(|sv| sv.version().as_u64() <= max_version)
}

All storage-layer versions are Version::Txn. This is enforced by:

  1. put_with_version(key, value, version: u64) wraps as Version::txn(version)sharded.rs:1169
  2. delete_with_version(key, version: u64) creates tombstone with Version::txn(version)sharded.rs:372
  3. Debug assertion in get_at_version()sharded.rs:85-87
  4. Debug assertion in history()sharded.rs:141-143

Comparison uses as_u64() (line 92), which is correct because all entries in a chain are the same variant (Txn).

5. Key Namespace Separation

Different primitives use different TypeTag values in their keys, preventing cross-primitive version chain mixing:

  Key structure: (BranchId, TypeTag, user_key_bytes)

  KV:     (branch, 0x01, "my-key")      → VersionChain A
  Event:  (branch, 0x02, "00000005")     → VersionChain B
  State:  (branch, 0x03, "my-cell")      → VersionChain C
  Branch: (branch, 0x05, "my-branch")    → VersionChain D
  Vector: (branch, 0x10, "my-vec")       → VersionChain E
  JSON:   (branch, 0x11, "my-doc")       → VersionChain F

Location: crates/core/src/types.rs:131-171

A KV key "foo" and a State cell "foo" are different keys in storage ((branch, 0x01, "foo") vs (branch, 0x03, "foo")). They occupy separate VersionChains. Mixed version types cannot collide.

6. Version Comparison Semantics by Context

ContextComparison MethodCorrect?
Storage get_at_version()as_u64() — raw numericYes — all entries are Txn
Storage gc()as_u64() — raw numericYes — all entries are Txn
Storage history()as_u64() — raw numericYes — all entries are Txn
Read-set validationu64 == u64Yes — both from same version space
State CAS (engine)Version == Version — full enumYes — both always Counter
State CAS (TransactionOps)Version != Version — full enumYes — both always Counter
Ord traitDiscriminant then valueN/A — not used in production paths

7. Version at the Executor Boundary

  Engine returns Version enum

       │  extract_version() / version_to_u64()
       │  Strips variant, returns raw u64

  Client receives u64

       │  Executor reconstructs variant
       │  (hardcoded per command type)

  Engine receives Version enum

Location: crates/executor/src/bridge.rs:273-280, crates/executor/src/convert.rs:135-142

Reconstruction by command type

CommandClient sendsExecutor wraps as
StateCasexpected_counter: Option<u64>Version::Counter(v)
EventGetsequence: u64Raw u64 (sequence number, not Version)
KvPutN/A (no expected version)N/A

The executor layer knows which variant each command uses and reconstructs correctly. A client cannot cause a variant mismatch because the wrapping is hardcoded.

8. Problems Found

Problem 1: VectorStore insert() and get() return different Version variants

Severity: Medium

// store.rs:459 — insert returns Counter
Ok(Version::counter(record_version))

// store.rs:519 — get returns Txn
version: Version::txn(record.version),

The same record.version (a raw u64) is wrapped as Version::counter() on insert but Version::txn() on get. The client sees the same numeric value, but:

  • If code ever compares these with full enum equality, they won’t match
  • The Version variant carries semantic meaning — Counter means “per-entity mutation count” while Txn means “transaction commit version”
  • This inconsistency could cause confusion in any future code that inspects the variant

Problem 2: EventGetByType silently returns version 0 for non-Sequence variants

Severity: Low

// handlers/event.rs:57-59
version: match e.version {
    Version::Sequence(s) => s,
    _ => 0,                    // Silent fallback
},

If an event’s version is not Version::Sequence (which should never happen in practice), the handler silently returns 0 instead of using as_u64() or returning an error. All other event handlers use extract_version() which handles all variants. This handler has a special-case that could mask bugs.

Problem 3: Client loses version type information (existing #930)

Severity: Low (already filed)

extract_version() and version_to_u64() strip the variant tag. The client receives VersionConflict { expected: 5, actual: 7 } with no indication of whether these are counters, transaction IDs, or sequence numbers. Already documented in issue #930.

Problem 4: Debug assertion is the only guard on storage version type

Severity: Low

The invariant that all storage-layer versions are Version::Txn is protected only by debug_assert!, which is stripped in release builds. If a code path ever stores a non-Txn version, the assertion would catch it in debug mode but silently corrupt version ordering in release mode.

In practice, this is safe because put_with_version() unconditionally wraps as Version::txn(). But the guard relies on the API contract, not on type-system enforcement.

9. Correctness Assessment

Can mixed version types produce wrong snapshot reads?

No. The design prevents this through three layers of defense:

  1. Key namespace separation: Different primitives use different TypeTags, so their VersionChains never overlap.

  2. Storage API wraps as Txn: put_with_version() always creates Version::txn(version), regardless of what the primitive uses internally.

  3. Primitive versions are inside the Value: Counter and Sequence versions are serialized inside the Value payload (State struct, Event struct, etc.), not at the storage version level.

Can version counter spaces collide?

No. Each counter space is independent:

CounterSourceScope
MVCC version (Txn)TransactionManager.version AtomicU64Global — shared by all primitives
Event sequenceEventLogMeta.next_sequencePer-branch, per-event-log
State counterState.version Counter(n)Per-cell
JSON counterJsonDoc.versionPer-document
Vector counterVectorRecord.versionPer-record
Branch counterBranchMetadata.versionPer-branch

The MVCC version and the primitive-specific version are stored at different levels (outer vs inner). A State cell with Counter(3) stored at Txn(42) in the storage layer has no interaction with a KV key stored at Txn(43).

10. Summary

#FindingSeverityType
1VectorStore insert returns Counter, get returns Txn for same versionMediumBug — inconsistent variant
2EventGetByType silently returns version 0 for non-Sequence variantsLowDefensive coding gap
3Client loses version type information (existing #930)LowContext loss
4Storage version type invariant protected only by debug_assertLowDesign — not type-enforced

Overall: The version system is correctly designed. The two-level architecture (outer Txn for MVCC, inner Counter/Sequence for primitive semantics) is sound. Key namespace separation prevents cross-primitive contamination. The only real bug is the vector variant inconsistency (#1), which affects correctness if code ever inspects the variant rather than the raw value.