LeVCS/crates/levcs-store/examples/phase1_frame_golden.rs

491 lines
17 KiB
Rust

//! Golden frame vector generator (scope 4-A1 deliverable 6).
//!
//! `cargo run -p levcs-store --example phase1_frame_golden > \
//! crates/levcs-store/tests/fixtures/phase1-frames.json`
//!
//! This is the **only** way `tests/fixtures/phase1-frames.json` is produced.
//! `tests/frame_golden.rs` includes this file as a module and asserts the
//! fixture matches a fresh generation byte for byte, which is what turns
//! "encoding is deterministic" into a check rather than a claim.
//!
//! Every input here is a constant. No clock, no randomness, no environment: a
//! vector that changed because the machine changed would be worthless as a
//! format freeze.
use levcs_core::{ObjectId, ObjectType};
use levcs_protocol::v2::{
CommittedTransactionV1, DurabilityResultV1, ProjectionMode, PushKindV2, PushOperationV2,
RefMutation, RefStateV1, RefTarget, RepoSnapshotV1, SignedClientOperationV2,
SignedCommittedTransactionV1, SignedPushOperationV2, SignedRepoSnapshotV1, SourceKindV1,
StagedProjectionInstallV1, TransactionEvidenceV1, TypedRefCas,
};
use levcs_store::format::{
digest, Frame, FrameHeader, FrameObjectV1, FrameObjectsV1, FrameReceiptFieldsV1,
RepositoryCreateV1, TransactionFramePayloadV1, FRAME_PAYLOAD_DIGEST_DOMAIN,
};
/// Fixed journal identity for every vector. A frame is bound to its journal,
/// so the corpus pins one.
pub const GOLDEN_JOURNAL_ID: [u8; 16] = [0x4A; 16];
fn main() {
println!(
"{}",
serde_json::to_string_pretty(&golden_value()).expect("serialize golden vectors")
);
}
// ---------------------------------------------------------------------------
// Deterministic building blocks
// ---------------------------------------------------------------------------
pub fn id(byte: u8) -> ObjectId {
ObjectId([byte; 32])
}
fn key(byte: u8) -> [u8; 32] {
[byte; 32]
}
/// A fabricated signature.
///
/// The store never verifies a signature — plan §5.1 forbids it from making
/// identity decisions, and scope §1 forbids any call into `levcs-identity`.
/// These vectors therefore carry signature-shaped bytes and assert only that
/// the store transports them byte for byte, which is exactly the property the
/// frame format is responsible for.
fn signature(byte: u8) -> [u8; 64] {
[byte; 64]
}
fn ref_set(name: &str, expected: Option<u8>, new: u8, force: bool) -> TypedRefCas {
TypedRefCas {
target: RefTarget::Branch(name.into()),
expected: expected.map(id),
mutation: RefMutation::Set(id(new)),
force,
}
}
fn ref_delete(name: &str, expected: u8) -> TypedRefCas {
TypedRefCas {
target: RefTarget::Release(name.into()),
expected: Some(id(expected)),
mutation: RefMutation::Delete,
force: false,
}
}
fn committed(
source_kind: SourceKindV1,
repo_sequence: u64,
old_authority: u8,
new_authority: u8,
) -> CommittedTransactionV1 {
CommittedTransactionV1 {
repo_id: id(0x20),
repo_sequence,
previous_event_digest: id(0x21),
sequenced_at_micros: 1_700_000_000_000_000,
source_kind,
actor: key(0x22),
operation_id: [0x23; 16],
operation_digest: id(0x24),
signed_evidence_digest: id(0x25),
old_authority: id(old_authority),
new_authority: id(new_authority),
refs: Vec::new(),
object_ids: Vec::new(),
commit_ids: Vec::new(),
resulting_state_digest: id(0x26),
}
}
fn signed_committed(transaction: CommittedTransactionV1) -> SignedCommittedTransactionV1 {
SignedCommittedTransactionV1 {
transaction,
source_key_epoch: 7,
durability_result: DurabilityResultV1::LocallyDurable,
source_signature: signature(0x27),
}
}
fn push_operation() -> PushOperationV2 {
PushOperationV2 {
operation_id: [0x11; 16],
repo_id: id(0x20),
issued_at_micros: 1_000,
retry_until_micros: 5_000,
nonce: [0x12; 16],
expected_authority: id(4),
authority_update: Some(id(5)),
updates: vec![ref_set("main", Some(6), 7, false)],
pack_len: 128,
pack_hash: id(9),
snapshot_generation_digest: id(10),
projection_stage: None,
kind: PushKindV2::Normal,
}
}
fn client_evidence() -> TransactionEvidenceV1 {
TransactionEvidenceV1::ClientV2 {
signed_envelope: SignedClientOperationV2::Push(SignedPushOperationV2 {
operation: push_operation(),
signer: key(0x13),
signature: signature(0x14),
}),
}
}
fn mirror_event_evidence() -> TransactionEvidenceV1 {
// `validate_structure` requires the outer epoch to equal the signed
// event's epoch, so the two are written from one value.
let epoch = 7;
TransactionEvidenceV1::MirrorEventV1 {
source_instance: key(0x30),
source_key_epoch: epoch,
source_snapshot_digest: id(0x31),
source_event: SignedCommittedTransactionV1 {
transaction: committed(SourceKindV1::Client, 4, 0x32, 0x33),
source_key_epoch: epoch,
durability_result: DurabilityResultV1::LocallyDurable,
source_signature: signature(0x34),
},
}
}
fn mirror_snapshot_evidence() -> TransactionEvidenceV1 {
let source_instance = key(0x40);
let epoch = 9;
TransactionEvidenceV1::MirrorSnapshotV1 {
source_instance,
source_key_epoch: epoch,
source_snapshot: SignedRepoSnapshotV1 {
snapshot: RepoSnapshotV1 {
source_instance,
source_key_epoch: epoch,
repo_id: id(0x41),
genesis_authority: id(0x42),
current_authority: id(0x43),
projection: ProjectionMode::Full,
refs: vec![RefStateV1 {
target: RefTarget::Branch("main".into()),
object: id(0x44),
}],
event_low_repo_sequence: 2,
object_low_repo_sequence: 1,
high_repo_sequence: 12,
high_event_digest: id(0x45),
resulting_state_digest: id(0x46),
config_epoch: 3,
capabilities_digest: id(0x47),
},
signature: signature(0x48),
},
destination_projection: ProjectionMode::Release,
projected_manifest_digest: id(0x49),
projected_object_count: 25,
projected_object_bytes: 4_096,
}
}
fn legacy_migration_evidence() -> TransactionEvidenceV1 {
TransactionEvidenceV1::LegacyMigrationV1 {
actor: key(0x50),
actor_key_epoch: 2,
migration_id: [0x51; 16],
repo_id: id(0x52),
chunk_ordinal: 1,
chunk_count: 4,
chunk_digest: id(0x53),
manifest_digest: id(0x54),
source_layout_digest: id(0x55),
signature: signature(0x56),
}
}
fn projection_admin_evidence() -> TransactionEvidenceV1 {
TransactionEvidenceV1::ProjectionAdminV1 {
actor: key(0x60),
actor_key_epoch: 4,
command_digest: id(0x61),
previous_projection: ProjectionMode::Full,
new_projection: ProjectionMode::Metadata,
signature: signature(0x62),
}
}
fn administrative_evidence() -> TransactionEvidenceV1 {
TransactionEvidenceV1::AdministrativeV1 {
actor: key(0x70),
actor_key_epoch: 5,
command_digest: id(0x71),
signature: signature(0x72),
}
}
fn evidence_for(kind: SourceKindV1) -> TransactionEvidenceV1 {
// Exhaustive: a new source kind must break this generator rather than
// quietly leave the corpus one vector short.
match kind {
SourceKindV1::Client => client_evidence(),
SourceKindV1::MirrorSnapshot => mirror_snapshot_evidence(),
SourceKindV1::MirrorEvent => mirror_event_evidence(),
SourceKindV1::LegacyMigration => legacy_migration_evidence(),
SourceKindV1::ProjectionAdmin => projection_admin_evidence(),
SourceKindV1::Administrative => administrative_evidence(),
}
}
pub const ALL_SOURCE_KINDS: [SourceKindV1; 6] = [
SourceKindV1::Client,
SourceKindV1::MirrorSnapshot,
SourceKindV1::MirrorEvent,
SourceKindV1::LegacyMigration,
SourceKindV1::ProjectionAdmin,
SourceKindV1::Administrative,
];
fn base_payload(kind: SourceKindV1) -> TransactionFramePayloadV1 {
TransactionFramePayloadV1 {
repository_create: None,
objects: FrameObjectsV1::Inline(vec![FrameObjectV1 {
object_type: ObjectType::Blob,
object_id: id(0x80),
raw: vec![0xAB; 16],
}]),
ref_cas: vec![ref_set("main", Some(6), 7, false)],
expected_authority: id(4),
new_authority: id(4),
evidence: evidence_for(kind),
committed: signed_committed(committed(kind, 1, 4, 4)),
receipt: FrameReceiptFieldsV1 {
objects_new: 1,
retry_until_micros: 5_000,
first_receipt_visibility_micros: 1_700_000_000_000_000,
},
}
}
// ---------------------------------------------------------------------------
// The corpus
// ---------------------------------------------------------------------------
struct Vector {
name: &'static str,
description: &'static str,
frame: Frame,
}
fn frame_from(
shard_sequence: u64,
repo_sequence: u64,
namespace: u8,
operation: u8,
payload: Vec<u8>,
) -> Frame {
let payload_len = payload.len() as u64;
Frame {
header: FrameHeader {
flags: 0,
total_len: levcs_store::format::frame_total_len(payload_len),
journal_id: GOLDEN_JOURNAL_ID,
shard_sequence,
repo_sequence,
namespace: [namespace; 32],
operation_id: [operation; 16],
operation_digest: id(operation),
payload_len,
payload_digest: digest(FRAME_PAYLOAD_DIGEST_DOMAIN, &payload),
},
payload,
}
}
fn encode(payload: &TransactionFramePayloadV1) -> Vec<u8> {
payload.encode_canonical().expect("canonical frame payload")
}
fn vectors() -> Vec<Vector> {
let mut out = Vec::new();
// The physical minimum: a zero-length payload, `total_len == 224`. A
// `Frame` carries opaque payload bytes precisely so completeness stays a
// physical property, and this vector pins the smallest thing the
// completeness definition must accept.
out.push(Vector {
name: "minimum-frame",
description: "smallest physically valid frame: empty payload, total_len 224",
frame: frame_from(0, 0, 0x00, 0x00, Vec::new()),
});
// Repository create.
let mut payload = base_payload(SourceKindV1::Client);
payload.repository_create = Some(RepositoryCreateV1 {
genesis_authority: id(0x90),
genesis_len: 777,
genesis_hash: id(0x91),
projection: ProjectionMode::Full,
});
out.push(Vector {
name: "repository-create",
description: "carries repository-create metadata (genesis authority, length, hash)",
frame: frame_from(1, 0, 0x01, 0x01, encode(&payload)),
});
// Multiple inline objects, strictly ascending by ObjectId.
let mut payload = base_payload(SourceKindV1::Client);
payload.objects = FrameObjectsV1::Inline(vec![
FrameObjectV1 {
object_type: ObjectType::Blob,
object_id: id(0xA0),
raw: vec![0x01; 16],
},
FrameObjectV1 {
object_type: ObjectType::Tree,
object_id: id(0xA1),
raw: vec![0x02; 24],
},
FrameObjectV1 {
object_type: ObjectType::Commit,
object_id: id(0xA2),
raw: vec![0x03; 32],
},
]);
payload.receipt.objects_new = 3;
out.push(Vector {
name: "multi-object",
description: "three inline new objects of three embedded types",
frame: frame_from(2, 1, 0x02, 0x02, encode(&payload)),
});
// A multi-ref transaction mixing Set and Delete. Multi-ref atomicity is a
// Phase 1 exit criterion, and it starts with the whole CAS set living in
// one frame.
let mut payload = base_payload(SourceKindV1::Client);
payload.ref_cas = vec![
ref_set("main", Some(0xB0), 0xB1, false),
ref_set("topic/one", None, 0xB2, false),
ref_delete("v1.0", 0xB3),
ref_set("feature-x", Some(0xB4), 0xB5, true),
];
out.push(Vector {
name: "multi-ref-set-and-delete",
description: "four typed ref CAS entries mixing Set, create-only Set, forced Set, Delete",
frame: frame_from(3, 2, 0x03, 0x03, encode(&payload)),
});
// Authority transition: expected != new.
let mut payload = base_payload(SourceKindV1::Client);
payload.expected_authority = id(0xC0);
payload.new_authority = id(0xC1);
payload.committed = signed_committed(committed(SourceKindV1::Client, 3, 0xC0, 0xC1));
out.push(Vector {
name: "authority-transition",
description: "expected and new current authority differ",
frame: frame_from(4, 3, 0x04, 0x04, encode(&payload)),
});
// Staged projection install: exactly one descriptor and no inline object.
let mut payload = base_payload(SourceKindV1::ProjectionAdmin);
payload.objects = FrameObjectsV1::StagedProjectionInstall(StagedProjectionInstallV1 {
session_id: [0xD0; 16],
manifest_digest: id(0xD1),
projection: ProjectionMode::Release,
object_count: 1_000,
object_bytes: 4_194_304,
membership_root: id(0xD2),
artifact_set_digest: id(0xD3),
});
payload.receipt.objects_new = 1_000;
out.push(Vector {
name: "staged-projection-install",
description: "one StagedProjectionInstallV1 descriptor instead of inline objects",
frame: frame_from(5, 4, 0x05, 0x05, encode(&payload)),
});
// One frame per SourceKindV1.
for (index, kind) in ALL_SOURCE_KINDS.into_iter().enumerate() {
let payload = base_payload(kind);
let name: &'static str = match kind {
SourceKindV1::Client => "source-kind-client",
SourceKindV1::MirrorSnapshot => "source-kind-mirror-snapshot",
SourceKindV1::MirrorEvent => "source-kind-mirror-event",
SourceKindV1::LegacyMigration => "source-kind-legacy-migration",
SourceKindV1::ProjectionAdmin => "source-kind-projection-admin",
SourceKindV1::Administrative => "source-kind-administrative",
};
out.push(Vector {
name,
description: "one frame per SourceKindV1, with the matching evidence variant",
frame: frame_from(
6 + index as u64,
5 + index as u64,
0x10 + index as u8,
0x10 + index as u8,
encode(&payload),
),
});
}
// Maximum-sequence edges. `shard_sequence` and `repo_sequence` are
// independent domains (plan §4 transaction invariant 8), so each maximum
// is its own vector rather than one frame carrying both.
let payload = base_payload(SourceKindV1::Client);
out.push(Vector {
name: "max-shard-sequence",
description: "shard_sequence == u64::MAX",
frame: frame_from(u64::MAX, 6, 0x30, 0x30, encode(&payload)),
});
let payload = base_payload(SourceKindV1::Client);
out.push(Vector {
name: "max-repo-sequence",
description: "repo_sequence == u64::MAX",
frame: frame_from(12, u64::MAX, 0x31, 0x31, encode(&payload)),
});
let payload = base_payload(SourceKindV1::Client);
out.push(Vector {
name: "max-both-sequences",
description: "shard_sequence and repo_sequence both u64::MAX",
frame: frame_from(u64::MAX, u64::MAX, 0x32, 0x32, encode(&payload)),
});
out
}
/// The golden document. Stable key order comes from `serde_json`'s default
/// `BTreeMap`-backed object.
pub fn golden_value() -> serde_json::Value {
let vectors: Vec<serde_json::Value> = vectors()
.into_iter()
.map(|vector| {
let bytes = vector.frame.encode().expect("golden frames must encode");
let header = &vector.frame.header;
serde_json::json!({
"name": vector.name,
"description": vector.description,
"shard_sequence": header.shard_sequence.to_string(),
"repo_sequence": header.repo_sequence.to_string(),
"namespace": hex::encode(header.namespace),
"operation_id": hex::encode(header.operation_id),
"operation_digest": hex::encode(header.operation_digest.as_bytes()),
"journal_id": hex::encode(header.journal_id),
"payload_len": header.payload_len.to_string(),
"total_len": header.total_len.to_string(),
"payload_digest": hex::encode(header.payload_digest.as_bytes()),
"frame_digest": hex::encode(&bytes[bytes.len() - 32..]),
"frame_hex": hex::encode(&bytes),
})
})
.collect();
serde_json::json!({
"storage_version": levcs_store::format::STORAGE_VERSION,
"frame_header_len": levcs_store::format::FRAME_HEADER_LEN,
"frame_trailer_len": levcs_store::format::FRAME_TRAILER_LEN,
"journal_header_len": levcs_store::format::JOURNAL_HEADER_LEN,
"frames": vectors,
})
}