Stamp Knowledge.acquired_at with the revision a commit actually resolves to (PA.f49)

Every `Knowledge.acquired_at` write computed `state.revision + 1` inline, which
is right for every commit except genesis: genesis applies at revision 0 and
resolves to revision 0 (`resulting_revision: 0`, `state.revision` never
advances past 0 for the creating commit), so a fact a genesis patch both
declared and granted knowledge of landed at `acquired_at: 1` — one revision
ahead of the state it was written into.

`apply_operation` and `apply_operations` now take the resolution revision as
an explicit `resolves_to` parameter instead of deriving it locally. Genesis
passes `state.revision` (0); every other caller passes
`state.revision + 1` through a small `resolution_revision` helper, computed
once per `apply_effect` branch instead of four times inline. Callers that
invoke `apply_operation`/`apply_operations` directly for a forged effect
(tests, and `action.rs`'s component-writer probe) now supply the same value
explicitly.

`verify_state_shape` in journal.rs gained a companion check: a knowledge
row's `acquired_at` must not exceed the world's own revision, so a forged or
corrupt row of this shape cannot pass replay silently.

Added three tests: a genesis-seeded knowledge row is acquired at revision 0,
a later commit's row is acquired at that commit's resolved revision, and the
shape check refuses a row forged ahead of the world's revision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-22 13:52:37 +02:00
commit 7d59e41869
3 changed files with 189 additions and 39 deletions

View file

@ -2523,8 +2523,10 @@ mod tests {
},
);
let mut refused = forged.clone();
let error = crate::apply_operations(&mut refused, std::slice::from_ref(&step), &[])
.unwrap_err();
let resolves_to = refused.revision + 1;
let error =
crate::apply_operations(&mut refused, std::slice::from_ref(&step), &[], resolves_to)
.unwrap_err();
assert!(matches!(error, KernelError::Invariant(_)));
let mut admitted = forged;
@ -2535,7 +2537,8 @@ mod tests {
over: crate::AuthorityTarget::PlaceSubtree(bench.civic.hall),
}]),
);
crate::apply_operations(&mut admitted, std::slice::from_ref(&step), &[])
let resolves_to = admitted.revision + 1;
crate::apply_operations(&mut admitted, std::slice::from_ref(&step), &[], resolves_to)
.expect("the named key opens the door at the component writer too");
assert_eq!(admitted.positions[&walker].place, bench.civic.chamber);
}

View file

@ -645,13 +645,16 @@ pub(super) fn verify_state_shape(state: &WorldState) -> Result<(), JournalError>
}
}
for (subject_id, held) in &state.knowledge {
let entry_is_live = |entry: &super::Knowledge| match entry.source {
super::KnowledgeSource::Told { by, via } => {
state.subjects.contains_key(&by)
&& via.is_none_or(|channel| state.channels.contains_key(&channel))
}
super::KnowledgeSource::Seen { by } => state.subjects.contains_key(&by),
super::KnowledgeSource::Witnessed | super::KnowledgeSource::Evidenced => true,
let entry_is_live = |entry: &super::Knowledge| {
entry.acquired_at <= state.revision
&& match entry.source {
super::KnowledgeSource::Told { by, via } => {
state.subjects.contains_key(&by)
&& via.is_none_or(|channel| state.channels.contains_key(&channel))
}
super::KnowledgeSource::Seen { by } => state.subjects.contains_key(&by),
super::KnowledgeSource::Witnessed | super::KnowledgeSource::Evidenced => true,
}
};
if !state.subjects.contains_key(subject_id)
|| held.is_empty()
@ -660,7 +663,8 @@ pub(super) fn verify_state_shape(state: &WorldState) -> Result<(), JournalError>
.any(|(fact, entry)| !state.facts.contains_key(fact) || !entry_is_live(entry))
{
return Err(JournalError::Corrupt(
"knowledge does not name a canonical subject, fact, and source".into(),
"knowledge does not name a canonical subject, fact, and source, or was acquired ahead of the world's revision"
.into(),
));
}
}

View file

@ -1979,7 +1979,11 @@ impl WorldState {
"genesis effect rules or mints without the play authority".into(),
)
})?;
admit_resolved(&mut state, resolved)?;
// Genesis resolves to revision 0 itself: `state.revision` never
// advances past 0 for the creating commit (`resulting_revision: 0`),
// unlike every later commit, which resolves to `state.revision + 1`.
let genesis_revision = state.revision;
admit_resolved(&mut state, resolved, genesis_revision)?;
state.state_digest = state_digest(&state)?;
Ok(state)
}
@ -1989,7 +1993,11 @@ impl WorldState {
/// through it in one fixed order — entities, routes, subjects, operations — so a
/// patch may relocate along a route it declares. It re-derives every structural
/// claim from `state`, so an effect that skipped resolution dies here.
fn admit_resolved(state: &mut WorldState, resolved: &ResolvedPatch) -> Result<(), KernelError> {
fn admit_resolved(
state: &mut WorldState,
resolved: &ResolvedPatch,
resolves_to: u64,
) -> Result<(), KernelError> {
if resolved.declares_nothing() && resolved.operations.is_empty() {
return Err(KernelError::Invariant(
"admitted patch carries no canonical change".into(),
@ -2211,7 +2219,7 @@ fn admit_resolved(state: &mut WorldState, resolved: &ResolvedPatch) -> Result<()
if let Some(weights) = &resolved.lens_weights {
state.lens_weights = weights.clone();
}
apply_operations(state, &resolved.operations, &resolved.evidence)
apply_operations(state, &resolved.operations, &resolved.evidence, resolves_to)
}
/// The component writer for every lane: snapshot the before-totals for every
@ -2228,6 +2236,7 @@ fn apply_operations(
state: &mut WorldState,
operations: &[ResolvedOp],
evidence: &[EvidenceRef],
resolves_to: u64,
) -> Result<(), KernelError> {
let mut deltas: BTreeMap<EntityId, LedgerDelta> = BTreeMap::new();
for operation in operations {
@ -2236,7 +2245,7 @@ fn apply_operations(
}
}
for operation in operations {
apply_operation(state, operation, evidence)?;
apply_operation(state, operation, evidence, resolves_to)?;
match operation {
ResolvedOp::Transform {
from_resource,
@ -2367,6 +2376,7 @@ fn apply_operation(
state: &mut WorldState,
operation: &ResolvedOp,
evidence: &[EvidenceRef],
resolves_to: u64,
) -> Result<(), KernelError> {
let insufficient = || KernelError::Invariant("holder does not hold enough".into());
let overflow = || KernelError::Invariant("holding would overflow".into());
@ -2798,12 +2808,13 @@ fn apply_operation(
}
// Matches `DecisionEvent.revision` and the `minted_at` scan built
// from it (`action::exercise`, `snapshot`): both name the revision
// this commit resolves to, one past `state.revision` here, which
// still holds the *previous* revision until `submit` bumps it.
let acquired_at = state
.revision
.checked_add(1)
.ok_or_else(|| KernelError::Serialization("world revision overflow".into()))?;
// this commit resolves to. `resolves_to` is `state.revision` itself
// at genesis, which never advances past 0, and `state.revision + 1`
// everywhere else, since `state` still holds the *previous*
// revision until `submit` bumps it. One value, one caller-computed
// source, so a genesis-seeded row cannot land one revision ahead of
// the state it was written into.
let acquired_at = resolves_to;
let entry = Knowledge {
confidence: *confidence,
source,
@ -2847,12 +2858,11 @@ fn apply_operation(
// An empty fan-out is legal: the delta of a telling is a property of
// the world — an empty room, a silenced channel — not a defect in
// the proposal. Speaking alone commits the claim and lands nothing.
// `acquired_at` matches `minted_at`: one past `state.revision`, the
// revision this commit resolves to (PA.f44).
let acquired_at = state
.revision
.checked_add(1)
.ok_or_else(|| KernelError::Serialization("world revision overflow".into()))?;
// `acquired_at` matches `minted_at` (PA.f44): both name the
// revision this commit resolves to, `resolves_to` (PA.f49) —
// `state.revision` itself at genesis, `state.revision + 1`
// otherwise.
let acquired_at = resolves_to;
for listener in fan_out(state, *speaker, *fact, to) {
state.knowledge.entry(listener).or_default().insert(
*fact,
@ -2885,10 +2895,13 @@ fn apply_operation(
"an actor displays from no place".into(),
));
}
let acquired_at = state
.revision
.checked_add(1)
.ok_or_else(|| KernelError::Serialization("world revision overflow".into()))?;
// The revision the caller's own commit resolves to: `state.revision`
// itself at genesis, which never advances past 0, and
// `state.revision + 1` everywhere else, since `state` still holds
// the *previous* revision until `submit` bumps it. One value, one
// caller-computed source, so a genesis-seeded row cannot land one
// revision ahead of the state it was written into.
let acquired_at = resolves_to;
for viewer in fan_out(state, *actor, *fact, &room) {
state.knowledge.entry(viewer).or_default().insert(
*fact,
@ -2926,10 +2939,13 @@ fn apply_operation(
"a witness reaches nobody who does not already hold the fact".into(),
));
}
let acquired_at = state
.revision
.checked_add(1)
.ok_or_else(|| KernelError::Serialization("world revision overflow".into()))?;
// The revision the caller's own commit resolves to: `state.revision`
// itself at genesis, which never advances past 0, and
// `state.revision + 1` everywhere else, since `state` still holds
// the *previous* revision until `submit` bumps it. One value, one
// caller-computed source, so a genesis-seeded row cannot land one
// revision ahead of the state it was written into.
let acquired_at = resolves_to;
for witness in recipients {
state.knowledge.entry(witness).or_default().insert(
*fact,
@ -5131,6 +5147,17 @@ fn validate_assignment(value: &ControllerAssignment) -> Result<(), KernelError>
Ok(())
}
/// The revision a non-genesis commit resolves to: one past `state.revision`,
/// which still holds the *previous* revision here, since `submit` only bumps
/// it after `apply_effect` returns. Genesis is not a caller: it resolves to
/// `state.revision` itself and computes that directly (PA.f49).
fn resolution_revision(state: &WorldState) -> Result<u64, KernelError> {
state
.revision
.checked_add(1)
.ok_or_else(|| KernelError::Serialization("world revision overflow".into()))
}
/// `command_id` is the band draw's only per-command term, so the arm that
/// re-derives an exercised decision needs the same one `reduce` drew from.
fn apply_effect(
@ -5191,7 +5218,8 @@ fn apply_effect(
}
_ => 0,
};
admit_resolved(state, resolved)?;
let resolves_to = resolution_revision(state)?;
admit_resolved(state, resolved, resolves_to)?;
// What makes "a commit clears exactly the boundary it answers"
// structural rather than hoped for. "Nothing else clears one" is
// true by construction: boundaries are derived and no writer
@ -5263,7 +5291,8 @@ fn apply_effect(
}
state.events.push(event.clone());
if !event.effects.is_empty() {
apply_operations(state, &event.effects, &[])?;
let resolves_to = resolution_revision(state)?;
apply_operations(state, &event.effects, &[], resolves_to)?;
}
state
.last_opportunity_at
@ -11736,6 +11765,7 @@ mod witness_tests {
confidence: Confidence::Certain,
},
&[],
state_revision + 1,
)
.expect("the witness lands");
assert_eq!(state.knowledge[&world.villager][&world.asteroid], held);
@ -11758,6 +11788,7 @@ mod witness_tests {
let directory = tempfile::tempdir().unwrap();
let (mut kernel, world, active) = nesting_kernel(directory.path(), "Nobody");
let forged = |place: EntityId, state: &WorldState| {
let resolves_to = state.revision + 1;
let mut state = state.clone();
apply_operation(
&mut state,
@ -11767,6 +11798,7 @@ mod witness_tests {
confidence: Confidence::Certain,
},
&[],
resolves_to,
)
.unwrap_err()
};
@ -11821,6 +11853,7 @@ mod witness_tests {
// A forged effect naming a place that is not one, or no live fact, is
// refused by the arm's own kind check before any of that.
let mut state = kernel.state.clone();
let resolves_to = state.revision + 1;
assert!(matches!(
apply_operation(
&mut state,
@ -11830,6 +11863,7 @@ mod witness_tests {
confidence: Confidence::Certain,
},
&[],
resolves_to,
)
.unwrap_err(),
KernelError::Invariant(message) if message == "a witness names no live fact or place"
@ -11889,6 +11923,7 @@ mod witness_tests {
vec![Mismatch::NoOperationEffect { operation: 0 }]
);
let mut state = kernel.state.clone();
let resolves_to = state.revision + 1;
assert!(matches!(
apply_operations(
&mut state,
@ -11898,6 +11933,7 @@ mod witness_tests {
confidence: Confidence::Certain,
}],
&[],
resolves_to,
)
.unwrap_err(),
KernelError::Invariant(message)
@ -12271,6 +12307,106 @@ mod witness_tests {
);
}
/// PA.f49: `acquired_at` stamps the revision the write's own commit
/// resolves to on every path, genesis included. Genesis resolves to
/// revision 0 itself — `state.revision` never advances past 0 for the
/// creating commit — so a fact a genesis patch both declares and grants
/// knowledge of lands at `acquired_at: 0`, not one revision ahead of the
/// state it was written into.
#[test]
fn a_genesis_seeded_knowledge_row_is_acquired_at_revision_zero() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("world.cc");
let mut creation = creation(CommandId::new(), "PAf49Genesis");
creation.patch.declarations.push(Declaration::Fact(FactDeclaration {
handle: DraftHandle::new("founding"),
label: "The Founding Ruling".into(),
statement: Statement::new("Stated by the table.").unwrap(),
standing: FactStandingRef::Claimed {
by: Ref::Draft(DraftHandle::new("human")),
},
}));
creation.patch.operations.push(ComponentOp::AcquireKnowledge {
subject: Ref::Draft(DraftHandle::new("human")),
fact: Ref::Draft(DraftHandle::new("founding")),
source: AuthoredSource::Witnessed,
confidence: Confidence::Certain,
});
let (kernel, _) = WorldKernel::create(&path, creation, &auth_principal(owner())).unwrap();
assert_eq!(kernel.state.revision, 0);
let human = *kernel
.state
.subjects
.iter()
.find(|(_, subject)| subject.label == "The Human")
.expect("genesis admits the declared human")
.0;
let fact = *kernel
.state
.facts
.iter()
.find(|(_, record)| record.statement.as_str() == "Stated by the table.")
.expect("genesis admits the declared fact")
.0;
assert_eq!(
kernel.state.knowledge[&human][&fact].acquired_at,
0,
"a genesis-seeded row landed one revision ahead of the state it was written into"
);
assert!(journal::verify_state_shape(&kernel.state).is_ok());
}
/// PA.f49: every commit after genesis still stamps the revision it
/// resolves to, one past the state it was applied against.
#[test]
fn a_later_commits_knowledge_row_is_acquired_at_that_commits_revision() {
let directory = tempfile::tempdir().unwrap();
let (mut kernel, world, active) = nesting_kernel(directory.path(), "PAf49Later");
submit_owner(
&mut kernel,
&active,
operations(vec![ComponentOp::AcquireKnowledge {
subject: Ref::Existing(world.villager),
fact: Ref::Existing(world.asteroid),
source: AuthoredSource::Witnessed,
confidence: Confidence::Certain,
}]),
);
assert_eq!(kernel.state.revision, active.revision + 1);
assert_eq!(
kernel.state.knowledge[&world.villager][&world.asteroid].acquired_at,
active.revision + 1
);
}
/// PA.f49: the shape check refuses a knowledge row forged ahead of the
/// world's own revision — the check `verify_state_shape` gained alongside
/// the genesis fix, so a corrupt or forged `acquired_at` cannot pass
/// replay silently.
#[test]
fn a_shape_check_refuses_a_knowledge_row_forged_ahead_of_the_world_revision() {
let directory = tempfile::tempdir().unwrap();
let (mut kernel, world, active) = nesting_kernel(directory.path(), "PAf49Shape");
submit_owner(
&mut kernel,
&active,
operations(vec![witness(
world.asteroid,
world.region,
Confidence::Certain,
)]),
);
assert!(journal::verify_state_shape(&kernel.state).is_ok());
let mut forged = kernel.state.clone();
let entry = forged
.knowledge
.get_mut(&world.villager)
.and_then(|held| held.get_mut(&world.asteroid))
.expect("the witness landed on the villager");
entry.acquired_at = forged.revision + 1;
assert!(journal::verify_state_shape(&forged).is_err());
}
/// The two callers of the shared owners answer the same question. A
/// `Reach::Place` channel's fan-out and a witness over the same place select
/// the same subjects over the same state, so "under a place" and "already a
@ -12387,6 +12523,7 @@ mod witness_tests {
confidence: Confidence::Certain,
},
&[],
adrift_revision + 1,
)
.expect("the hemisphere still reaches the standers");
assert!(
@ -18417,10 +18554,16 @@ mod clock_tests {
.0;
let human = human_subject(&kernel.snapshot().unwrap());
let mut state = kernel.state.clone();
let resolves_to = state.revision + 1;
assert!(
matches!(
apply_operation(&mut state, &ResolvedOp::Retire { subject: human }, &[])
.unwrap_err(),
apply_operation(
&mut state,
&ResolvedOp::Retire { subject: human },
&[],
resolves_to
)
.unwrap_err(),
KernelError::Invariant(message)
if message == "retire operation would remove a Draft approver"
),