mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-30 02:57:34 +00:00
The v3.0.9 security guidance must provide an executable repair path for data accepted before sync verified payloads independently. Add an offline event-integrity-check command with JSON-lines reporting and explicit --execute. Scan the main, tombstone, holding and replacement-history databases, plus purgatory and rejected-event checkpoints, without skipping timestamp ties or expired LMDB payloads. Purge only invalid payloads and their linked internal metadata. Reuse deletion lifecycle request removal, but never create a NIP-09 tombstone for an untrusted claimed ID or follow forged tags into Git/cascade deletion. Preserve valid copies in other stores and pending placeholders. Reset payload-free negative caches because their decisions cannot be verified and could block recovery. The relay must be stopped for both modes; this deliberately avoids adding a live repair race or startup option. Checkpoints are parsed before mutation and rewritten atomically. Partial runs are retryable. Git side-effect reconstruction and policy authorization of correctly signed events remain separate concerns. Document backup, scan, purge and rescan commands alongside this change in the Unreleased changelog, including the existing Git integrity repair command. Validation: 1,010 library tests passed during development; the final five audit regressions, seven binary tests, and two real-command integration tests pass. All-target workspace Clippy with warnings denied, formatting and diff checks pass. Coverage includes forgery recovery, lifecycle metadata removal, expired payloads, timestamp ties, valid-copy preservation, checkpoint cache repair, missing paths and malformed-checkpoint refusal. Assisted-by: GPT-6
105 lines
3.3 KiB
Rust
105 lines
3.3 KiB
Rust
//! Exercise the actual offline command against persisted forged payloads.
|
|
use nostr_lmdb::NostrLmdb;
|
|
use nostr_sdk::prelude::*;
|
|
use tokio::process::Command;
|
|
|
|
async fn run(
|
|
relay: &std::path::Path,
|
|
git: &std::path::Path,
|
|
execute: bool,
|
|
) -> std::process::Output {
|
|
let mut command = Command::new(env!("CARGO_BIN_EXE_ngit-grasp"));
|
|
command
|
|
.kill_on_drop(true)
|
|
.arg("event-integrity-check")
|
|
.arg("--relay-data-path")
|
|
.arg(relay)
|
|
.arg("--git-data-path")
|
|
.arg(git);
|
|
if execute {
|
|
command.arg("--execute");
|
|
}
|
|
tokio::time::timeout(std::time::Duration::from_secs(30), command.output())
|
|
.await
|
|
.unwrap()
|
|
.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn command_reports_purges_and_allows_genuine_recovery() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let relay = root.path().join("relay");
|
|
let git = root.path().join("git");
|
|
std::fs::create_dir_all(&relay).unwrap();
|
|
std::fs::create_dir_all(&git).unwrap();
|
|
let genuine = EventBuilder::new(Kind::TextNote, "genuine")
|
|
.finalize(&Keys::generate())
|
|
.unwrap();
|
|
let mut forged = genuine.clone();
|
|
forged.content = "forged".into();
|
|
{
|
|
let db = NostrLmdb::builder(&relay)
|
|
.process_nip09(false)
|
|
.process_nip62(false)
|
|
.build()
|
|
.await
|
|
.unwrap();
|
|
db.save_event(&forged).await.unwrap();
|
|
}
|
|
let dry = run(&relay, &git, false).await;
|
|
assert!(!dry.status.success());
|
|
let lines: Vec<serde_json::Value> = String::from_utf8(dry.stdout)
|
|
.unwrap()
|
|
.lines()
|
|
.map(|line| serde_json::from_str(line).unwrap())
|
|
.collect();
|
|
assert_eq!(lines[0]["claimed_id"], genuine.id.to_hex());
|
|
assert_eq!(lines.last().unwrap()["stores"]["main"]["purged"], 0);
|
|
let checkpoint = git.join("purgatory-state.json");
|
|
std::fs::write(&checkpoint, "not a checkpoint").unwrap();
|
|
assert!(!run(&relay, &git, true).await.status.success());
|
|
{
|
|
let db = NostrLmdb::builder(&relay)
|
|
.process_nip09(false)
|
|
.process_nip62(false)
|
|
.build()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
db.event_by_id(&genuine.id).await.unwrap(),
|
|
Some(forged.clone())
|
|
);
|
|
}
|
|
std::fs::remove_file(&checkpoint).unwrap();
|
|
let repaired = run(&relay, &git, true).await;
|
|
assert!(
|
|
repaired.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&repaired.stderr)
|
|
);
|
|
let clean = run(&relay, &git, false).await;
|
|
assert!(
|
|
clean.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&clean.stderr)
|
|
);
|
|
let db = NostrLmdb::builder(&relay)
|
|
.process_nip09(false)
|
|
.process_nip62(false)
|
|
.build()
|
|
.await
|
|
.unwrap();
|
|
assert!(db.event_by_id(&genuine.id).await.unwrap().is_none());
|
|
db.save_event(&genuine).await.unwrap();
|
|
assert_eq!(db.event_by_id(&genuine.id).await.unwrap(), Some(genuine));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn command_rejects_nonexistent_database_without_creating_it() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let missing = root.path().join("missing");
|
|
let output = run(&missing, root.path(), true).await;
|
|
assert!(!output.status.success());
|
|
assert!(!missing.exists());
|
|
}
|