blob: 1dec5b96be801efb33f633e7ea29a1d537b46400 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::*;
use sha2::{Digest, Sha256};
#[tokio::test]
async fn a_rehashed_latest_checkpoint_cannot_hide_a_broken_predecessor_commitment() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("events.sqlite");
let log = EventLog::open(&path).await.unwrap();
let observed = closed(&log, "first-main", 128, 1).await;
log.append_batch(&prepared(&log, "first-compact").await)
.await
.unwrap();
closed(&log, "second-main", 128, 1).await;
let latest = prepared(&log, "second-compact").await;
log.append_batch(&latest).await.unwrap();
assert_eq!(
log.read_model_context("session", None)
.await
.unwrap()
.baseline
.unwrap()
.event_id,
latest[0].event().id
);
log.close().await.unwrap();
let inspect = rusqlite::Connection::open(&path).unwrap();
inspect
.execute(
"UPDATE event_log SET event_json=' ' || event_json WHERE event_id=?",
[&observed],
)
.unwrap();
let mut checkpoint_event = latest[0].event().clone();
let Fact::ContextCheckpointRecorded { checkpoint } = &mut checkpoint_event.fact else {
panic!()
};
// Rebuild only the newest commitment from the documented raw encoding.
// Its covered bytes include the unchanged old commitment, which is now
// false. A valid newest digest therefore cannot replace checking every cut.
let scope = serde_json::to_vec(&LogScope::Session {
id: "session".into(),
})
.unwrap();
let mut raw = Sha256::new();
raw.update(b"maka.log-prefix.v1\0");
raw.update((scope.len() as u64).to_be_bytes());
raw.update(scope);
let mut statement = inspect.prepare("SELECT sequence,event_json FROM session_history_events WHERE owner_session_id='session' AND sequence<=? ORDER BY sequence").unwrap();
for row in statement
.query_map([checkpoint.covered_through as i64], |row| {
Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
})
.unwrap()
{
let (sequence, json) = row.unwrap();
raw.update(sequence.to_be_bytes());
raw.update((json.len() as u64).to_be_bytes());
raw.update(json.as_bytes());
}
raw.update(checkpoint.covered_through.to_be_bytes());
checkpoint.source_digest = format!("sha256:{}", hex::encode(raw.finalize()));
let mut effective = Sha256::new();
effective.update(b"maka.effective-context.v1\0");
effective.update(checkpoint.source_digest.as_bytes());
let request_json: String = inspect.query_row("SELECT event_json FROM runtime_events WHERE invocation_id='second-compact' AND kind='model_requested'", [], |row| row.get(0)).unwrap();
let mut request: RuntimeEvent = serde_json::from_str(&request_json).unwrap();
let Fact::ModelRequested {
source_digest,
effective_source_digest,
..
} = &mut request.fact
else {
panic!()
};
*source_digest = checkpoint.source_digest.clone();
*effective_source_digest = Some(format!("sha256:{}", hex::encode(effective.finalize())));
for event in [&request, &checkpoint_event] {
inspect
.execute(
"UPDATE event_log SET event_json=? WHERE event_id=?",
rusqlite::params![serde_json::to_string(event).unwrap(), event.id],
)
.unwrap();
}
drop(statement);
drop(inspect);
let log = EventLog::open(&path).await.unwrap();
let error = log.read_model_context("session", None).await.unwrap_err();
assert!(
error.to_string().contains("source digest changed"),
"{error}"
);
log.close().await.unwrap();
}