diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-20 23:44:05 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-11-21 03:05:41 +0000 |
| commit | 2bbb7292c978d36464b6166faa78223677389ef6 (patch) | |
| tree | ef82eb6f9267c27a08e758d32112a33c13991717 /grasp-audit/src | |
| parent | 519fdc66930280cd1772417dca327ed858333d64 (diff) | |
Implement GRASP-01 stateful write policy with database queries
- Add Nip34WritePolicy with Arc<MemoryDatabase> for stateful event validation
- Implement full GRASP-01 event acceptance policy:
* Accept events referencing accepted repositories (via a, A, q tags)
* Accept events referencing accepted events (transitive, via e, E, q tags)
* Support forward references (events referenced by accepted events)
* Reject orphan events with no valid references
- Extract and validate all reference tag types (a, A, q, e, E)
- Query database for repository and event existence checks
- Implement fail-secure error handling for database query failures
Test improvements:
- Fix send_and_verify_rejected to handle relay rejection errors properly
- Fix RepoWithIssue fixture usage in forward reference tests
- Add database synchronization polling for race condition mitigation
- Achieve 94% test pass rate (16/17 integration tests passing)
Diffstat (limited to 'grasp-audit/src')
| -rw-r--r-- | grasp-audit/src/specs/grasp01/event_acceptance_policy.rs | 76 |
1 files changed, 52 insertions, 24 deletions
diff --git a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs index 638ae5f..c1977f9 100644 --- a/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs +++ b/grasp-audit/src/specs/grasp01/event_acceptance_policy.rs | |||
| @@ -551,11 +551,36 @@ impl EventAcceptancePolicyTests { | |||
| 551 | ) -> Result<(), String> { | 551 | ) -> Result<(), String> { |
| 552 | let event_id = event.id; | 552 | let event_id = event.id; |
| 553 | 553 | ||
| 554 | client | 554 | // Try to send event - rejection may cause send_event to fail with an error |
| 555 | .send_event(event) | 555 | let send_result = client.send_event(event).await; |
| 556 | .await | 556 | |
| 557 | .map_err(|e| format!("Failed to send event to relay: {}", e))?; | 557 | // If send succeeded, the relay might have accepted it (we'll verify below) |
| 558 | // If send failed, check if it's a rejection error (expected) | ||
| 559 | if let Err(e) = send_result { | ||
| 560 | let err_msg = e.to_string().to_lowercase(); | ||
| 561 | // Check if error message indicates rejection (not network/other errors) | ||
| 562 | if err_msg.contains("rejected") || err_msg.contains("blocked") { | ||
| 563 | // Expected rejection - verify event is NOT in database | ||
| 564 | tokio::time::sleep(Duration::from_millis(100)).await; | ||
| 565 | |||
| 566 | let filter = Filter::new().id(event_id); | ||
| 567 | let events = client | ||
| 568 | .query(filter) | ||
| 569 | .await | ||
| 570 | .map_err(|e| format!("Failed to query relay for verification: {}", e))?; | ||
| 571 | |||
| 572 | if !events.is_empty() { | ||
| 573 | return Err(format!("Event was rejected but still stored: {}", description)); | ||
| 574 | } | ||
| 575 | |||
| 576 | return Ok(()); // Rejected as expected | ||
| 577 | } else { | ||
| 578 | // Unexpected error (network, etc.) | ||
| 579 | return Err(format!("Failed to send event to relay: {}", e)); | ||
| 580 | } | ||
| 581 | } | ||
| 558 | 582 | ||
| 583 | // Send succeeded, verify event was NOT stored (relay should have rejected) | ||
| 559 | tokio::time::sleep(Duration::from_millis(100)).await; | 584 | tokio::time::sleep(Duration::from_millis(100)).await; |
| 560 | 585 | ||
| 561 | let filter = Filter::new().id(event_id); | 586 | let filter = Filter::new().id(event_id); |
| @@ -877,6 +902,26 @@ impl EventAcceptancePolicyTests { | |||
| 877 | ) | 902 | ) |
| 878 | })?; | 903 | })?; |
| 879 | 904 | ||
| 905 | // Verify repo is queryable (ensures it's fully indexed before we reference it) | ||
| 906 | let repo_id = Self::extract_d_tag(&repo).ok_or("Failed to extract repo_id")?; | ||
| 907 | let verify_filter = Filter::new() | ||
| 908 | .kind(Kind::GitRepoAnnouncement) | ||
| 909 | .author(repo.pubkey) | ||
| 910 | .identifier(repo_id); | ||
| 911 | |||
| 912 | // Poll until repo is available (with timeout) | ||
| 913 | for _ in 0..10 { | ||
| 914 | let events = client.query(verify_filter.clone()).await | ||
| 915 | .map_err(|e| format!("Failed to verify repo: {}", e))?; | ||
| 916 | if !events.is_empty() { | ||
| 917 | break; | ||
| 918 | } | ||
| 919 | tokio::time::sleep(Duration::from_millis(50)).await; | ||
| 920 | } | ||
| 921 | |||
| 922 | // Extra delay to ensure relay's internal database is fully synchronized | ||
| 923 | tokio::time::sleep(Duration::from_millis(200)).await; | ||
| 924 | |||
| 880 | // Create Kind 1 note locally but DON'T send it yet | 925 | // Create Kind 1 note locally but DON'T send it yet |
| 881 | let kind1_note = client | 926 | let kind1_note = client |
| 882 | .event_builder(Kind::TextNote, "Note to be referenced") | 927 | .event_builder(Kind::TextNote, "Note to be referenced") |
| @@ -938,34 +983,17 @@ impl EventAcceptancePolicyTests { | |||
| 938 | // Create TestContext | 983 | // Create TestContext |
| 939 | let ctx = TestContext::new(client); | 984 | let ctx = TestContext::new(client); |
| 940 | 985 | ||
| 941 | // Get repo with issue fixture (mode-aware) | 986 | // Get issue fixture (mode-aware) - RepoWithIssue returns the issue event directly |
| 942 | let repo = ctx | 987 | let issue = ctx |
| 943 | .get_fixture(FixtureKind::RepoWithIssue) | 988 | .get_fixture(FixtureKind::RepoWithIssue) |
| 944 | .await | 989 | .await |
| 945 | .map_err(|e| { | 990 | .map_err(|e| { |
| 946 | format!( | 991 | format!( |
| 947 | "Test setup failed: could not get repo with issue fixture: {}", | 992 | "Test setup failed: could not get issue fixture: {}", |
| 948 | e | 993 | e |
| 949 | ) | 994 | ) |
| 950 | })?; | 995 | })?; |
| 951 | 996 | ||
| 952 | // Extract the issue from the repo event (it's stored as the first 'e' tag) | ||
| 953 | let issue_id = repo | ||
| 954 | .tags | ||
| 955 | .iter() | ||
| 956 | .find(|t| t.kind() == TagKind::e()) | ||
| 957 | .and_then(|t| t.content()) | ||
| 958 | .ok_or("Missing issue reference in RepoWithIssue fixture")?; | ||
| 959 | |||
| 960 | // Query to get the actual issue event | ||
| 961 | let filter = Filter::new().id(nostr_sdk::EventId::from_hex(issue_id) | ||
| 962 | .map_err(|e| format!("Invalid issue ID: {}", e))?); | ||
| 963 | let issues = client | ||
| 964 | .query(filter) | ||
| 965 | .await | ||
| 966 | .map_err(|e| format!("Failed to query issue: {}", e))?; | ||
| 967 | let issue = issues.first().ok_or("Issue not found")?.clone(); | ||
| 968 | |||
| 969 | // Create Comment A locally but DON'T send it yet | 997 | // Create Comment A locally but DON'T send it yet |
| 970 | let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?; | 998 | let comment_a = Self::create_comment_for_event(client, &issue, "Comment A")?; |
| 971 | 999 | ||