lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 93879c2a8f9696fc6453267ea427d4a9e9f63ffc
parent 023a88f96da7895a10c240704849c79b90e80a34
Author: triesap <tyson@radroots.org>
Date:   Sun, 19 Jul 2026 21:04:44 +0000

event: evaluate NIP-09 suppression

- add a pure evaluator over verified targets and admitted requests
- enforce same-author event references and inclusive address cutoffs
- return canonical deterministic decisions and supporting evidence
- execute a fixed signed suppression corpus and exact operation authority

Diffstat:
MCHANGELOG.md | 5+++++
Acontracts/conformance/vectors/deletion/suppression.v1.json | 609+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/event_boundary_matrix.md | 31+++++++++++++++++++++++--------
Mcontracts/events/social-events.md | 47++++++++++++++++++++++++++++++++++++-----------
Mcontracts/operations.toml | 35+++++++++++++++++++++++++++++++++++
Mcontracts/releases/1.0.0-alpha.1.toml | 11+++++++++++
Mcrates/event_codec/README | 29+++++++++++++++++++++++++----
Acrates/event_codec/src/deletion/evaluator.rs | 754+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/event_codec/src/deletion/mod.rs | 1+
Acrates/event_codec/tests/fixtures/deletion_suppression.v1.json | 609+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_codec/tests/nip09_suppression_conformance.rs | 587+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/contract.rs | 680+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mtools/xtask/src/contract/deletion_authority.rs | 75++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
13 files changed, 3385 insertions(+), 88 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -159,6 +159,11 @@ publish policy both pass for the same source revision. creates and transports a request only; it provides no target lookup, authorship decision, deletion authorization, store mutation, relay-effect, or deletion-effect semantics. +- NIP-09 suppression evaluation is now a separate pure operation over one + signature-verified candidate and admitted requests. It enforces same-author + direct-event and inclusive address-cutoff rules, keeps kind `5` immune, + ignores advisory kinds, returns canonical evidence independent of input + order, and never mutates raw events or storage. ### Removed diff --git a/contracts/conformance/vectors/deletion/suppression.v1.json b/contracts/conformance/vectors/deletion/suppression.v1.json @@ -0,0 +1,609 @@ +{ + "suite": "nip09_suppression_evaluator", + "contract_version": "1.0.0", + "vectors": [ + { + "id": "nip09_suppress_no_requests_visible", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_no_authorized_reference" + } + }, + { + "id": "nip09_suppress_same_author_event_reference", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"fade0f4ddcd9f2f8b9c6508055ceb58c687bd8b99cdaf5f4b63cc13b16f75278\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"event reference a\",\"sig\":\"36ab6c25bcc0fa22da55a566121cf43210f90a8f0f9243736fd24ff57b4799ac09cf017ef3cd132464b728bb5ff87603535fbb1b566413ed5f34a60c1838a809\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": { + "request_id": "fade0f4ddcd9f2f8b9c6508055ceb58c687bd8b99cdaf5f4b63cc13b16f75278" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + }, + { + "id": "nip09_suppress_same_author_nonmatching_reference", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"9b3d3a1ca7f4a7019a17c86465cadfeaa262ebc76042d2a5a07971e09b36d14c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"120bd6d1a82eb11b24f05316f3254c0fa6cb87fcc909d7200a963e40207f5e77\"]],\"content\":\"other reference\",\"sig\":\"9765abc98be250a2b60b6f3aa33e695ff58e0aa5f5b2e927cbb878acdb25b645a8d1a9d03f6cb9de69448c4ec6bc7e6f2cc5276a3865ac0a4d4c8daf4b331a48\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_no_authorized_reference" + } + }, + { + "id": "nip09_suppress_wrong_author_exact_event_reference", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"403981275c07a7c67d35552ee3dbc416c20b770d5485216ff2d1b18ad287588b\",\"pubkey\":\"e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"wrong author event\",\"sig\":\"e7f2e84135c979c0d3fa815d3b9082163152e37d55cf0f40e21e4df81baee81efe1365b0011da940b63efb7dc9d13fb7d84d990aee4a04e4dd582790ff205770\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_request_author_mismatch" + } + }, + { + "id": "nip09_suppress_event_reference_predates_target", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"a75e12b2175feafcad0763545cbbf31c118915f60d97572bb68fafbcc533f6be\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100190,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"pre-target exact event\",\"sig\":\"c2fbe879659b5da522dbb400ac08174f522482a8077fa10c868c1a395598e2e40175d1f232964d897f84e9e710de8c9a2b0feff088cead33a5d1879a96fa1d7d\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": { + "request_id": "a75e12b2175feafcad0763545cbbf31c118915f60d97572bb68fafbcc533f6be" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + }, + { + "id": "nip09_suppress_deletion_request_immune_event_reference", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"a8fb77cd006005bffaa04f49ba01560dccfc2ca9875d79bd0a7bdc3862553df5\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"a9bbfa0344854ced08a13230a3a84b2d3971f5bf08f1f6af25f1547eec307a89\"]],\"content\":\"delete a deletion\",\"sig\":\"52a7356d38a5a49c32094905a5b4ff5c4adb41146a6b046377541cd91cf44b2f0cdb7640aab556c8375fafc8460f6f3076731d12b9923cc15538d54dcdd47f6a\"}" + ], + "target_event_json": "{\"id\":\"a9bbfa0344854ced08a13230a3a84b2d3971f5bf08f1f6af25f1547eec307a89\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":5,\"tags\":[[\"e\",\"120bd6d1a82eb11b24f05316f3254c0fa6cb87fcc909d7200a963e40207f5e77\"]],\"content\":\"original deletion\",\"sig\":\"5f17a714f308ca33c2d5f1e4b52d2a56a406474ae0e6a1cd4482b3a3cf116fdab7cebe77e5a6cc18a2490f32292bcdf4df08ab8d3a2d507df4cd99c39e6fb1f7\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_request_immune" + } + }, + { + "id": "nip09_suppress_deletion_request_immune_mixed_references", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"6cebea08ac79c820dac9936444a818dd8aa89dffb5b0b251b21edea266463860\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100211,\"kind\":5,\"tags\":[[\"e\",\"a9bbfa0344854ced08a13230a3a84b2d3971f5bf08f1f6af25f1547eec307a89\"],[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"mixed deletion target\",\"sig\":\"464b930b47df90bf5c9d6f0138c082d71c3fb84f49755168e690672cc1a47635c507518f876cc9aceff60765c3db9a8be1b774a3322b79dbe82bd45f3c06de8b\"}" + ], + "target_event_json": "{\"id\":\"a9bbfa0344854ced08a13230a3a84b2d3971f5bf08f1f6af25f1547eec307a89\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":5,\"tags\":[[\"e\",\"120bd6d1a82eb11b24f05316f3254c0fa6cb87fcc909d7200a963e40207f5e77\"]],\"content\":\"original deletion\",\"sig\":\"5f17a714f308ca33c2d5f1e4b52d2a56a406474ae0e6a1cd4482b3a3cf116fdab7cebe77e5a6cc18a2490f32292bcdf4df08ab8d3a2d507df4cd99c39e6fb1f7\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_request_immune" + } + }, + { + "id": "nip09_suppress_address_cutoff_before_target", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"1473dabefc767c7a2a16ce716e0cbcd1ea463299c6e1e4d1c805672669012bd2\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100199,\"kind\":5,\"tags\":[[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"address before\",\"sig\":\"7bdf964d33f45eaeb81fbc671ae994dfdcf61dcac6c37f70831be76148ff9e05a2a0abe673a8655a2d3acaffdfba5aaa719cc00a26b9f3938626839681e991dd\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100199, + "request_id": "1473dabefc767c7a2a16ce716e0cbcd1ea463299c6e1e4d1c805672669012bd2" + }, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_address_cutoff_precedes_target" + } + }, + { + "id": "nip09_suppress_address_cutoff_equal_target", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"3d75aa21ad472076c331419a3f2e73284ef8083f79d11f47b4c79e76604dc775\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":5,\"tags\":[[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"address equal\",\"sig\":\"aeda08b49690df9655012e964b69dca4c9f5f1545423378581875b8fb1ea27719ee0ba1c6c54a06c6938dec8b967379403c45d43e208b6746715f0cdf8c26e73\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100200, + "request_id": "3d75aa21ad472076c331419a3f2e73284ef8083f79d11f47b4c79e76604dc775" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_address_cutoff_after_target", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"086915e7b1c08ba14f5c8fabca8490b56b022b093ce21490d8f706df83c13e2c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100201,\"kind\":5,\"tags\":[[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"address after\",\"sig\":\"49dc8bd554da237d73ac266f893895df0509e4cf3b5eb372b393c42b8872bf1afb1d1922be622acbf6b811657d15beac4f6a2e69d24206b185db3135ed6cdb70\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100201, + "request_id": "086915e7b1c08ba14f5c8fabca8490b56b022b093ce21490d8f706df83c13e2c" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_address_wrong_kind", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"b24aa726605e907b6d2733e8e36e47f890e544583c976fa0347b054d706e74ec\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"3:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"wrong kind\",\"sig\":\"a03a7de08ba411613602fd0d8d04f70dbe81dd194bb7a556691595a2830a0334883cb0b45d9e6ddb253aed25de405da567c4c9f48a2064172ddef88959b7209c\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_no_authorized_reference" + } + }, + { + "id": "nip09_suppress_address_wrong_identifier", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"8388ea59c1f08f5091b3332e172145ef09bc671e7ad71e44c198867a66578824\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"39999:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:orchard:east/plot\"]],\"content\":\"wrong identifier\",\"sig\":\"85f06ad83418d262150642d1b64eea745f410232c6589d7cde7fd63fd896fd66b644db93a275cbeceadebf0f4976ee9cdee38ef2e6f84c76364e9309a4c614bb\"}" + ], + "target_event_json": "{\"id\":\"198aeb65fa856b3eb7b5a7fe36fa90da70fbdbb1cb88b31a3893d07c9437c796\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":39999,\"tags\":[[\"d\",\"orchard:west/plot\"]],\"content\":\"opaque addressable\",\"sig\":\"cc3db13caa8e407e842d5a977f720624a17364bf617179aa5a9318e7ab111818b82d20799e074092522fa49c8b709e4b8af1877a0599c63797f81a71689b9638\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_no_authorized_reference" + } + }, + { + "id": "nip09_suppress_address_wrong_pubkey", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"39ab140373199234d51f13034f0383a6d7a492d49303cf0b5c08f31d876674f6\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"0:e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af:\"]],\"content\":\"wrong pubkey\",\"sig\":\"efc60d415f344ce7103ad9179a6642c5823caf2e4b44f642276130f8be7e9400d882ce1299bdf898bdbfc01efd105f525c2a3dc55dd0d56531b74ed173b171a6\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_no_authorized_reference" + } + }, + { + "id": "nip09_suppress_wrong_author_address_reference", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"616960881e5bdaa21bff166269193e8503fc0d8f55507ec955a55421c34c46c5\",\"pubkey\":\"e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"wrong author address\",\"sig\":\"515d51aca57cecaeb94111daf508979c6b78523ef4005ffed9966a39beeaea26c01b727482f82ddea88db8f3333f4e00dd6fa1ad4eddd0ec43ba213bb44f96f5\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_request_author_mismatch" + } + }, + { + "id": "nip09_suppress_replaceable_kind_0", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"086915e7b1c08ba14f5c8fabca8490b56b022b093ce21490d8f706df83c13e2c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100201,\"kind\":5,\"tags\":[[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"address after\",\"sig\":\"49dc8bd554da237d73ac266f893895df0509e4cf3b5eb372b393c42b8872bf1afb1d1922be622acbf6b811657d15beac4f6a2e69d24206b185db3135ed6cdb70\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100201, + "request_id": "086915e7b1c08ba14f5c8fabca8490b56b022b093ce21490d8f706df83c13e2c" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_replaceable_kind_3", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"e10db54031bbff5da53d70d7bba7f6ee3ce4497152618cad9e2ea78567874169\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"3:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"replaceable kind 3\",\"sig\":\"816d45c35ebdef5495ec1060f52fad19ea88b4d093431e1ae194ca5e7380145950b2b03dea2dd701e13a4e99c0c6071bb2e45e59782ce3a0547ef8a4ff6dadab\"}" + ], + "target_event_json": "{\"id\":\"9b7a698850a451d198e792be95a10bb093ccfd65ebbdc8395d4282c930f281ae\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":3,\"tags\":[],\"content\":\"contacts\",\"sig\":\"30e46ce6dc57368992a86e2eace20470164de87aeecc08c9717231af4b75eb45e6945fc3b07be3cbf640a2b7fb75c9811903f94027bbe39374cc1a06155a4aea\"}" + }, + "expected": { + "address_reference": { + "coordinate": "3:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100220, + "request_id": "e10db54031bbff5da53d70d7bba7f6ee3ce4497152618cad9e2ea78567874169" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_replaceable_kind_10000", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"e512278a22f532facc56739af02486b579a680b7a71ab0e064b86875049f79b1\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"10000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"replaceable kind 10000\",\"sig\":\"b814886b0f702015d37ed545e14771ae9c712ca57ad7e81437add6e109060d346b1695c7abb6b1810198d89af6fe0a50ee5eb54ed4676024652276932ea0d4de\"}" + ], + "target_event_json": "{\"id\":\"db73709e2333601fa97d8ade507c1fb046d37129d32b99e4ff24b316895399e1\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":10000,\"tags\":[],\"content\":\"replaceable lower\",\"sig\":\"8152e75fefbfa5e361402a70c4ee796d5cc6e11cf98ea447e2e7f53a8ad6b8618edea1dcdd8194deaf2304c5e53b442f56fc9e8809b062d0f6a1f07ea1caddb2\"}" + }, + "expected": { + "address_reference": { + "coordinate": "10000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100220, + "request_id": "e512278a22f532facc56739af02486b579a680b7a71ab0e064b86875049f79b1" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_replaceable_kind_19999", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"9bafe3e4a5ea4bc2f6c2027c096bab9962949d0a172003a3177f7739dc1e55eb\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"19999:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"replaceable kind 19999\",\"sig\":\"8d8ecdcf165819bff0f173289a8ad13758b812f36a12b43de37f1cea468a893e89ae542121b6e3d3fb75b52a946ef2ac81dfad209350ece01b4548d4a04a61cc\"}" + ], + "target_event_json": "{\"id\":\"22ae552d598bd60490890db49e4738fc6d10376dc9fb9765fb319b3d3d72a472\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":19999,\"tags\":[],\"content\":\"replaceable upper\",\"sig\":\"16db6c2afd5eaf97143d0ecf1ab6535c2df08e1d61dffcf2feabea54dca1a42d3f11b1d82ac77bb60b4498469a25823c1adc6ae133a00ddf77c86ba6f6ccbb60\"}" + }, + "expected": { + "address_reference": { + "coordinate": "19999:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100220, + "request_id": "9bafe3e4a5ea4bc2f6c2027c096bab9962949d0a172003a3177f7739dc1e55eb" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_addressable_kind_30000_empty_identifier", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"9900ec21cbddfe6598f64983cf52232ff3b59250fa41810dc8ca883a22c21d49\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"addressable kind 30000\",\"sig\":\"a8cee6e2fe394cf26ac783a57d275a8891f716cc8077a85e488fd02d04c9c821ce65e98543b022b1cf62733d2e7a0fbfdfaaec2c49bad68f761d49dfd09ad262\"}" + ], + "target_event_json": "{\"id\":\"8cb1b211dc3b8bfbd7f457ffe8f4b067f0b065c2658126ee65a82f43ffb67c5a\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable initial\",\"sig\":\"a60cffce8efd7b1a1372d6d0b175afc49bcd1e7a88b94f3e57ef212a4b7f9dbbb91c058c2af11209a20b2cd30d6ce74886fcf770c726dc8c3e20d0081e0ec1fa\"}" + }, + "expected": { + "address_reference": { + "coordinate": "30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100220, + "request_id": "9900ec21cbddfe6598f64983cf52232ff3b59250fa41810dc8ca883a22c21d49" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_addressable_kind_39999_opaque_identifier", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"9a308e463030f356e68abefba2bc9c766c766f509ec6809c8709ebe7da8d355e\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"39999:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:orchard:west/plot\"]],\"content\":\"addressable kind 39999\",\"sig\":\"bd7b5f63aaf54a80283da21103556e235851b9e31e5b668808ceff8f6d1af303b4334b7855478b277bca7da1775a630b46671ca195afd1f920752a7cb391afb2\"}" + ], + "target_event_json": "{\"id\":\"198aeb65fa856b3eb7b5a7fe36fa90da70fbdbb1cb88b31a3893d07c9437c796\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":39999,\"tags\":[[\"d\",\"orchard:west/plot\"]],\"content\":\"opaque addressable\",\"sig\":\"cc3db13caa8e407e842d5a977f720624a17364bf617179aa5a9318e7ab111818b82d20799e074092522fa49c8b709e4b8af1877a0599c63797f81a71689b9638\"}" + }, + "expected": { + "address_reference": { + "coordinate": "39999:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:orchard:west/plot", + "inclusive_cutoff": 1800100220, + "request_id": "9a308e463030f356e68abefba2bc9c766c766f509ec6809c8709ebe7da8d355e" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_combined_event_and_address_references", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"eec90d8cb7e493d68b197310454c1f5b92828af5973aa26ee8d5fb82e0f30434\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"e\",\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\"],[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"combined references\",\"sig\":\"3f8203c5d57289e9ebdc5c2e2ab7f487ee60e409d5d4667057eba0ad10153c59aa8236d37e7d177a693243f9c9896cf05f8afd971dfc358438a55cc3ff5ebfaf\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100220, + "request_id": "eec90d8cb7e493d68b197310454c1f5b92828af5973aa26ee8d5fb82e0f30434" + }, + "event_reference": { + "request_id": "eec90d8cb7e493d68b197310454c1f5b92828af5973aa26ee8d5fb82e0f30434" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_and_address_reference" + } + }, + { + "id": "nip09_suppress_event_reference_with_stale_address", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"a0165c75548878cd6a959ab2a12e3826c1b118240c328cc0470d441b7306edf0\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100199,\"kind\":5,\"tags\":[[\"e\",\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\"],[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"exact event stale address\",\"sig\":\"512a96994d5f357abd9cd9c7cc7a00658ec4ee7cbe4bb6d83fbe26b22c75755f92c43f89b4ff8f12ce0c06e44fa1277c370aac1de4551543831a6640cb1496f4\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100199, + "request_id": "a0165c75548878cd6a959ab2a12e3826c1b118240c328cc0470d441b7306edf0" + }, + "event_reference": { + "request_id": "a0165c75548878cd6a959ab2a12e3826c1b118240c328cc0470d441b7306edf0" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + }, + { + "id": "nip09_suppress_kind_advisory_diagnostics_ignored", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"859689351e1457702ea9fb97e056185781bd779dc3889b42b9bed49936691ea9\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"],[\"k\"],[\"k\",\"bogus\"],[\"k\",\"1\"],[\"k\",\"1\"],[\"k\",\"2\"]],\"content\":\"ignored kind diagnostics\",\"sig\":\"5dfaa28b172be180313aee8e204b08a2a4a208f81ecdd1727f0d55bddfd57fbd62ac5cc0cbda5f17f8f4bd4bf758f77c0685723859d9269e0a2c7f39bde88354\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": { + "request_id": "859689351e1457702ea9fb97e056185781bd779dc3889b42b9bed49936691ea9" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + }, + { + "id": "nip09_suppress_duplicate_raw_targets_deduplicated", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"ab329bb4dcb4e7b3b706f44b1212e8464d0c1a622e0f7d4cb5da62b5b7291ade\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"e\",\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\"],[\"e\",\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\"],[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"],[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"duplicate raw references\",\"sig\":\"7479c3b44811e401d3cf51140779db9b2b9c6b04194f643e01c4b5f2f3adfa54812ebb1f36d83cdc5d8dcc72eb88335fae281c9383135dbe7da41c0e5a1287fe\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100220, + "request_id": "ab329bb4dcb4e7b3b706f44b1212e8464d0c1a622e0f7d4cb5da62b5b7291ade" + }, + "event_reference": { + "request_id": "ab329bb4dcb4e7b3b706f44b1212e8464d0c1a622e0f7d4cb5da62b5b7291ade" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_and_address_reference" + } + }, + { + "id": "nip09_suppress_max_address_cutoff_forward_order", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"88979696e9bbc7762dd224aa4a7c95dbe19567e0d83667185472a4124105c7aa\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100230,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 230\",\"sig\":\"420f8775b6e5454e8fb69207f00b1c9d215e3f3f837e3433d1c939acba5bb7dcea64db45edcfbeb3c621696cf9c58ecb469e298a2377ca28c399a8e2c5dd97bd\"}", + "{\"id\":\"2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100240,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 240 a\",\"sig\":\"7562e4477358a0788f7ca4052944fb647235910504de38280b3737b0e43cb1628beb4a19312d83e93438a131d9ab7239193696bc61d70e09e370c98023c45cd2\"}" + ], + "target_event_json": "{\"id\":\"8cb1b211dc3b8bfbd7f457ffe8f4b067f0b065c2658126ee65a82f43ffb67c5a\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable initial\",\"sig\":\"a60cffce8efd7b1a1372d6d0b175afc49bcd1e7a88b94f3e57ef212a4b7f9dbbb91c058c2af11209a20b2cd30d6ce74886fcf770c726dc8c3e20d0081e0ec1fa\"}" + }, + "expected": { + "address_reference": { + "coordinate": "30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100240, + "request_id": "2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_max_address_cutoff_reverse_order", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100240,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 240 a\",\"sig\":\"7562e4477358a0788f7ca4052944fb647235910504de38280b3737b0e43cb1628beb4a19312d83e93438a131d9ab7239193696bc61d70e09e370c98023c45cd2\"}", + "{\"id\":\"88979696e9bbc7762dd224aa4a7c95dbe19567e0d83667185472a4124105c7aa\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100230,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 230\",\"sig\":\"420f8775b6e5454e8fb69207f00b1c9d215e3f3f837e3433d1c939acba5bb7dcea64db45edcfbeb3c621696cf9c58ecb469e298a2377ca28c399a8e2c5dd97bd\"}" + ], + "target_event_json": "{\"id\":\"8cb1b211dc3b8bfbd7f457ffe8f4b067f0b065c2658126ee65a82f43ffb67c5a\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable initial\",\"sig\":\"a60cffce8efd7b1a1372d6d0b175afc49bcd1e7a88b94f3e57ef212a4b7f9dbbb91c058c2af11209a20b2cd30d6ce74886fcf770c726dc8c3e20d0081e0ec1fa\"}" + }, + "expected": { + "address_reference": { + "coordinate": "30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100240, + "request_id": "2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_later_revision_survives_address_cutoff", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"88979696e9bbc7762dd224aa4a7c95dbe19567e0d83667185472a4124105c7aa\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100230,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 230\",\"sig\":\"420f8775b6e5454e8fb69207f00b1c9d215e3f3f837e3433d1c939acba5bb7dcea64db45edcfbeb3c621696cf9c58ecb469e298a2377ca28c399a8e2c5dd97bd\"}", + "{\"id\":\"2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100240,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 240 a\",\"sig\":\"7562e4477358a0788f7ca4052944fb647235910504de38280b3737b0e43cb1628beb4a19312d83e93438a131d9ab7239193696bc61d70e09e370c98023c45cd2\"}" + ], + "target_event_json": "{\"id\":\"d35339c16ecb0baff3149324c96c01983e4263278a1e1d6f58ad41f538bcd3ed\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100260,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable later\",\"sig\":\"55e1bffad1fa0553c1b67f27a15f226c3ea18ba2a2716f12a8ae46cdd007d2c3b2a6d36997521de8a70cc2914dceb1f0fe1664176e27c458069f7c04b9383ad4\"}" + }, + "expected": { + "address_reference": { + "coordinate": "30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100240, + "request_id": "2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c" + }, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_address_cutoff_precedes_target" + } + }, + { + "id": "nip09_suppress_equal_max_cutoff_uses_lowest_request_id", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100240,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 240 a\",\"sig\":\"7562e4477358a0788f7ca4052944fb647235910504de38280b3737b0e43cb1628beb4a19312d83e93438a131d9ab7239193696bc61d70e09e370c98023c45cd2\"}", + "{\"id\":\"5bb1267cbc05f8c9365cc6f73ab79cc800c85fdfbdbe4e3d6d319574937244f6\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100240,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 240 b\",\"sig\":\"55d93ad2d0702eb9fdb8e019c5802b75ecef439951ab77c76b18bac5d09a2d465fc6e4618dcc43bb7bfb62690e9cae0d60c5dba99c8f3ab24f3231b4e45c8bdf\"}" + ], + "target_event_json": "{\"id\":\"8cb1b211dc3b8bfbd7f457ffe8f4b067f0b065c2658126ee65a82f43ffb67c5a\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable initial\",\"sig\":\"a60cffce8efd7b1a1372d6d0b175afc49bcd1e7a88b94f3e57ef212a4b7f9dbbb91c058c2af11209a20b2cd30d6ce74886fcf770c726dc8c3e20d0081e0ec1fa\"}" + }, + "expected": { + "address_reference": { + "coordinate": "30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100240, + "request_id": "2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_multiple_event_references_use_lowest_request_id", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"fade0f4ddcd9f2f8b9c6508055ceb58c687bd8b99cdaf5f4b63cc13b16f75278\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"event reference a\",\"sig\":\"36ab6c25bcc0fa22da55a566121cf43210f90a8f0f9243736fd24ff57b4799ac09cf017ef3cd132464b728bb5ff87603535fbb1b566413ed5f34a60c1838a809\"}", + "{\"id\":\"2284746bed9c30d7d7829553a42fa5e53200bf8bd6160a99076ee97e9b3aa75b\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100211,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"event reference b\",\"sig\":\"920891d1e65e9b476e0a0810cde3583bd92586c3e07feb650727ade2aff5487c08f0c37fc669bfd3137082991fc1ac0e467987c3b8468a11107b5b4fcd59a3a5\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": { + "request_id": "2284746bed9c30d7d7829553a42fa5e53200bf8bd6160a99076ee97e9b3aa75b" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + }, + { + "id": "nip09_suppress_repeated_request_is_idempotent", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"fade0f4ddcd9f2f8b9c6508055ceb58c687bd8b99cdaf5f4b63cc13b16f75278\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"event reference a\",\"sig\":\"36ab6c25bcc0fa22da55a566121cf43210f90a8f0f9243736fd24ff57b4799ac09cf017ef3cd132464b728bb5ff87603535fbb1b566413ed5f34a60c1838a809\"}", + "{\"id\":\"fade0f4ddcd9f2f8b9c6508055ceb58c687bd8b99cdaf5f4b63cc13b16f75278\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"event reference a\",\"sig\":\"36ab6c25bcc0fa22da55a566121cf43210f90a8f0f9243736fd24ff57b4799ac09cf017ef3cd132464b728bb5ff87603535fbb1b566413ed5f34a60c1838a809\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": { + "request_id": "fade0f4ddcd9f2f8b9c6508055ceb58c687bd8b99cdaf5f4b63cc13b16f75278" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + }, + { + "id": "nip09_suppress_unauthorized_event_plus_stale_authorized_address", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"23f5fa17f6c32fbac2be7d3bbac04f772c8736610c508bb058d5176f07fb83cd\",\"pubkey\":\"e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af\",\"created_at\":1800100270,\"kind\":5,\"tags\":[[\"e\",\"8cb1b211dc3b8bfbd7f457ffe8f4b067f0b065c2658126ee65a82f43ffb67c5a\"]],\"content\":\"unauthorized exact addressable event\",\"sig\":\"554ecdb5a70ae3a5a57689a531635876d71e66a9f39deaff4384f1f3a983824c11566c8e007487ef664abdde03f248b18404254f958c5cf20ae2d9cb0c78e7b6\"}", + "{\"id\":\"0e2e9cb53f3c98e254fdbba41f6f87f470dceed991d2fee7c06db5957690b182\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100199,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"stale authorized address\",\"sig\":\"2d3b1a3f296eb2558af31611426af71f705a503f6671f9528d6752214fbbbb2b7b47837b9acc7e3f69f1bd068058cad735ea48fbd6ff1f2a1f793eefad165907\"}" + ], + "target_event_json": "{\"id\":\"8cb1b211dc3b8bfbd7f457ffe8f4b067f0b065c2658126ee65a82f43ffb67c5a\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable initial\",\"sig\":\"a60cffce8efd7b1a1372d6d0b175afc49bcd1e7a88b94f3e57ef212a4b7f9dbbb91c058c2af11209a20b2cd30d6ce74886fcf770c726dc8c3e20d0081e0ec1fa\"}" + }, + "expected": { + "address_reference": { + "coordinate": "30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100199, + "request_id": "0e2e9cb53f3c98e254fdbba41f6f87f470dceed991d2fee7c06db5957690b182" + }, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_address_cutoff_precedes_target" + } + }, + { + "id": "nip09_suppress_exact_event_deletes_later_replacement", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"5d0e1e5fbd9f7f8afc20b7b53b7387fb5bbeb4cfaa9ab715833efc3ed248f83d\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100270,\"kind\":5,\"tags\":[[\"e\",\"d35339c16ecb0baff3149324c96c01983e4263278a1e1d6f58ad41f538bcd3ed\"]],\"content\":\"exact later replacement\",\"sig\":\"c7beed981d08bb5be1b1938baaac92554bfe0133ac1de85c56c0205c4adcd3f52f2895819faa2d8c1733fdad6589c6fba9547135e5b580a230881621f5d083b8\"}" + ], + "target_event_json": "{\"id\":\"d35339c16ecb0baff3149324c96c01983e4263278a1e1d6f58ad41f538bcd3ed\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100260,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable later\",\"sig\":\"55e1bffad1fa0553c1b67f27a15f226c3ea18ba2a2716f12a8ae46cdd007d2c3b2a6d36997521de8a70cc2914dceb1f0fe1664176e27c458069f7c04b9383ad4\"}" + }, + "expected": { + "address_reference": null, + "event_reference": { + "request_id": "5d0e1e5fbd9f7f8afc20b7b53b7387fb5bbeb4cfaa9ab715833efc3ed248f83d" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + } + ] +} diff --git a/contracts/event_boundary_matrix.md b/contracts/event_boundary_matrix.md @@ -211,19 +211,34 @@ unprovable, so a differing `k` advisory is not promoted to a conflict. Admission proves only that the kind-`5` request envelope and bounded request profile are valid. It performs no target lookup, same-author authorization, address cutoff evaluation, replacement or suppression decision, store -mutation, or deletion-request immunity evaluation. The event-contract registry -v7 classifies `radroots.social.deletion_request.v1` as `TypedOnly` for -authoring and `AdmissionOnly` for matching. Generic kind-`5` signing and client -publication fail before signer access. +mutation, or deletion-request immunity evaluation. + +The separate pure evaluator accepts a signature-verified candidate and +admitted deletion requests. It never mutates an event or store. Kind `5` is +immune; every other suppression requires equal request and candidate authors. +An exact `e` target is time-independent. An exact canonical `a` target applies +through the inclusive maximum qualifying request timestamp, so a later +replacement remains visible. Advisory `k` values are ignored. The decision and +its canonical evidence are independent of request order and repeated +qualifying inputs. + +The event-contract registry v7 classifies +`radroots.social.deletion_request.v1` as `TypedOnly` for authoring and +`AdmissionOnly` for matching. Generic kind-`5` signing and client publication +fail before signer access. The complete operation surface is exactly `social.deletion_request.build_authored_draft`, -`social.deletion_request.project_verified_event`, and -`social.deletion_request.verify_and_admit_event`. Content is limited to 131072 +`social.deletion_request.project_verified_event`, +`social.deletion_request.verify_and_admit_event`, and +`social.deletion_request.evaluate_suppression`. Content is limited to 131072 UTF-8 bytes, a request to 1024 tags, all tags to 4096 elements including names, each element to 4096 UTF-8 bytes, aggregate tag bytes to 131072, and compact -signed wire JSON to 262144 bytes. The canonical fixed 80-case corpus is +signed wire JSON to 262144 bytes. The canonical fixed 80-case request corpus is `contracts/conformance/vectors/deletion/verified_profile.v1.json`. +Pure effect evaluation has the separate canonical +`contracts/conformance/vectors/deletion/suppression.v1.json` corpus and does +not weaken or add effect fields to the request corpus. ## Coverage matrix @@ -234,7 +249,7 @@ signed wire JSON to 262144 bytes. The canonical fixed 80-case corpus is | post | 1 | RadrootsAuthoredUpdate / RadrootsAuthoredPhotoUpdate / RadrootsAuthoredAsk / RadrootsInboundPostProjection | events.post.publish, events.post.list, events.post.get | ordinary kind-1 reads remain generic; exact root-card subtypes require verified admission; any `e` tag produces a thread-excluded candidate without a Reply claim | | reply | 1 | RadrootsAuthoredNip10Reply / RadrootsInboundNip10ReplyProjection / RadrootsAdmittedNip10ReplyEvent / RadrootsNostrNip10ReplyEventBuilder | social.reply.build_authored_draft, social.reply.project_verified_event, social.reply.verify_and_admit_event | strict marked direct/nested NIP-10 authoring; verified marked or positional inbound admission with advisory-metadata diagnostics; never a root card; target existence, kind, author, and relay availability are not proven | | comment | 1111 | RadrootsAuthoredNip22Comment / RadrootsInboundNip22CommentProjection / RadrootsAdmittedNip22CommentEvent / RadrootsNostrNip22CommentEventBuilder | social.comment.build_authored_draft, social.comment.project_verified_event, social.comment.verify_and_admit_event | strict NIP-22 event/address roots limited to kinds `30402`, `31922`, and `31923`; tolerant verified projection; registry-v7 typed-only authoring and admission-only matching | -| deletion_request | 5 | RadrootsAuthoredNip09DeletionRequest / RadrootsInboundNip09DeletionProjection / RadrootsAdmittedNip09DeletionRequestEvent / RadrootsNostrNip09DeletionRequestEventBuilder | social.deletion_request.build_authored_draft, social.deletion_request.project_verified_event, social.deletion_request.verify_and_admit_event | effect-free NIP-09 request authoring and verified projection; event and replaceable/addressable targets; advisory kind diagnostics; registry-v7 typed-only authoring and admission-only matching | +| deletion_request | 5 | RadrootsAuthoredNip09DeletionRequest / RadrootsInboundNip09DeletionProjection / RadrootsAdmittedNip09DeletionRequestEvent / RadrootsNip09SuppressionDecision / RadrootsNostrNip09DeletionRequestEventBuilder | social.deletion_request.build_authored_draft, social.deletion_request.project_verified_event, social.deletion_request.verify_and_admit_event, social.deletion_request.evaluate_suppression | effect-free NIP-09 request authoring and verified projection plus pure immutable suppression evaluation; same-author direct event targets and inclusive address cutoffs; kind-5 immunity; advisory kinds ignored; registry-v7 typed-only authoring and admission-only matching | | reaction | 7 | RadrootsReaction | events.reaction.publish, events.reaction.list, events.reaction.get | requires event, pubkey, or address tags | | repost | 6 | RadrootsRepost | events.repost.publish, events.repost.list, events.repost.get | NIP-18 kind-1 repost surface | | generic_repost | 16 | RadrootsGenericRepost | events.generic_repost.publish, events.generic_repost.list, events.generic_repost.get | NIP-18 generic repost surface | diff --git a/contracts/events/social-events.md b/contracts/events/social-events.md @@ -36,11 +36,12 @@ authoring and admission profile are separate contract layers. The repository implements strict authored and verified-projected kind `1` root-post profiles, a separate strict-authored and tolerant-inbound kind `1` NIP-10 Reply profile, a strict-authored and tolerant verified-inbound kind `1111` NIP-22 Comment profile, a strict-authored and tolerant -verified-inbound kind `5` NIP-09 deletion-request profile, kind `7` `RadrootsReaction`, generic -`RadrootsList` entries, operational listing records through `RadrootsOperationalListing`, the raw -kind-`30402` profile partition and validated FoodAvailability authored, verified-admission, and -revision contract, articles, generic public file metadata, calendar date events, calendar time -events, reposts, generic reposts, calendar collections, RSVP events, and reports. +verified-inbound kind `5` NIP-09 deletion-request profile with pure deterministic suppression +evaluation, kind `7` `RadrootsReaction`, generic `RadrootsList` entries, operational listing +records through `RadrootsOperationalListing`, the raw kind-`30402` profile partition and validated +FoodAvailability authored, verified-admission, and revision contract, articles, generic public +file metadata, calendar date events, calendar time events, reposts, generic reposts, calendar +collections, RSVP events, and reports. The closeout contract requires: @@ -54,8 +55,8 @@ The closeout contract requires: `e_prev` authority - strict NIP-09 `RadrootsAuthoredNip09DeletionRequest`, `RadrootsInboundNip09DeletionProjection`, and - `RadrootsAdmittedNip09DeletionRequestEvent` request behavior without - deletion-effect authority + `RadrootsAdmittedNip09DeletionRequestEvent` request behavior plus a separate + pure `RadrootsNip09SuppressionDecision` evaluator without storage authority - strict NIP-25 `RadrootsReaction` behavior where empty content is a valid like - explicit optional `published_at` support for NIP-99 classified-listing parity - NIP-65 relay-list validation evidence through `RadrootsList` @@ -78,7 +79,8 @@ The MVP public social substrate includes: - strict `RadrootsAuthoredNip09DeletionRequest` publication plus `RadrootsInboundNip09DeletionProjection` and `RadrootsAdmittedNip09DeletionRequestEvent` for effect-free kind-`5` NIP-09 - event and address deletion requests + event and address deletion requests, followed by pure deterministic + suppression evaluation over verified targets and admitted requests - `RadrootsArticle` for NIP-23 kind `30023` long-form content - generic public `RadrootsFileMetadata` for NIP-94 kind `1063` - strict authored `RadrootsAuthoredCalendarDateEvent`, tolerant @@ -362,12 +364,27 @@ envelope whose NIP-01 id and Schnorr signature were verified. Admission proves only a valid bounded deletion request. It does not look up a target, establish same-author authorization, calculate an address cutoff, replace or suppress an event, mutate a store, or decide whether another deletion request is immune. -Those are downstream evaluator and storage responsibilities. +Those remain separate evaluator and storage responsibilities. + +`evaluate_nip09_suppression` is the pure evaluator boundary. It accepts one +signature-verified candidate event and a set of admitted deletion requests, +never mutates either input, reads no clock or store, and returns an explicit +decision with canonically ordered evidence. A kind-`5` candidate is always +immune. Every other candidate can be suppressed only by a request from the +same author. An `e` target is time-independent: an exact event-id match +qualifies even when the request predates the candidate. An `a` target must +match the candidate's canonical replaceable or addressable coordinate and +suppresses revisions whose `created_at` is less than or equal to the inclusive +maximum cutoff among qualifying requests. A later replacement remains +unsuppressed. Advisory `k` tags never participate in authorization or +suppression. Request ordering and repeated qualifying inputs cannot change the +decision. The complete public operation namespace is exactly `social.deletion_request.build_authored_draft`, -`social.deletion_request.project_verified_event`, and -`social.deletion_request.verify_and_admit_event`. Contract +`social.deletion_request.project_verified_event`, +`social.deletion_request.verify_and_admit_event`, and +`social.deletion_request.evaluate_suppression`. Contract `radroots.social.deletion_request.v1` is registry-v7 `TypedOnly` for authoring and `AdmissionOnly` for unsigned matching. Generic kind-`5` signing and client publication are reserved before signer access. @@ -383,6 +400,14 @@ contains no secret or private key, nsec, seed, generator, mutation, base, or boundary-expansion recipe, and expected projections expose no authorization, suppression, or store-mutation effect. +Suppression behavior is governed separately by +`contracts/conformance/vectors/deletion/suppression.v1.json` and its +byte-identical packaged mirror. Its fixed inputs exercise direct event targets, +inclusive address cutoffs, later replacements, requests that predate their +targets, kind-`5` immunity, unrelated authors, mixed qualifying requests, and +input-order independence. The evaluator produces evidence only; neither +fixture nor operation authorizes a store mutation. + `RadrootsReaction` uses strict NIP-25 semantics. Empty content, `+`, `-`, emoji, and custom reaction content are valid when the target tags are valid. Missing targets remain invalid. diff --git a/contracts/operations.toml b/contracts/operations.toml @@ -162,6 +162,11 @@ public = [ "RadrootsNip09DeletionProjectionError", "RadrootsAdmittedNip09DeletionRequestEvent", "RadrootsNip09DeletionAdmissionError", + "RadrootsNip09SuppressionOutcome", + "RadrootsNip09SuppressionReason", + "RadrootsNip09EventReferenceEvidence", + "RadrootsNip09AddressReferenceEvidence", + "RadrootsNip09SuppressionDecision", "RadrootsReaction", "RadrootsArticle", "RadrootsFileMetadata", @@ -1243,6 +1248,36 @@ case_kinds = [ "social.deletion_request.verify_and_admit_event.invalid", ] +[operations.social_deletion_request_evaluate_suppression] +domain = "social" +id = "social.deletion_request.evaluate_suppression" +stability = "beta" +inputs = [ + "RadrootsSignatureVerifiedEvent", + "RadrootsAdmittedNip09DeletionRequestEvent[]", +] +outputs = ["RadrootsNip09SuppressionDecision"] +error_class = "validation_error" +deterministic = true +signing = "none" +transport = "none" + +[operations.social_deletion_request_evaluate_suppression.implementation] +rust_modules = ["crates/event_codec/src/deletion/evaluator.rs"] +rust_types = [ + "radroots_event_codec::deletion::admission::RadrootsAdmittedNip09DeletionRequestEvent", + "radroots_event_codec::deletion::evaluator::RadrootsNip09AddressReferenceEvidence", + "radroots_event_codec::deletion::evaluator::RadrootsNip09EventReferenceEvidence", + "radroots_event_codec::deletion::evaluator::RadrootsNip09SuppressionDecision", + "radroots_event_codec::deletion::evaluator::RadrootsNip09SuppressionOutcome", + "radroots_event_codec::deletion::evaluator::RadrootsNip09SuppressionReason", + "radroots_event_codec::verification::RadrootsSignatureVerifiedEvent", +] + +[operations.social_deletion_request_evaluate_suppression.conformance] +vector = "contracts/conformance/vectors/deletion/suppression.v1.json" +case_kinds = ["social.deletion_request.evaluate_suppression.valid"] + [operations.social_reaction_build_tags] domain = "social" id = "social.reaction.build_tags" diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml @@ -253,3 +253,14 @@ semver_impacts = [ "change_exported_algorithm_behavior", ] summary = "Add strict effect-free NIP-09 deletion-request authoring, verified tolerant projection and admission, sealed publication, exact operation authority, and a fixed self-contained conformance corpus while advancing the event registry to version 7 and reserving generic kind-5 authoring." + +[[changes]] +id = "pure-nip09-suppression-evaluator" +classification = "feature" +semver_impacts = [ + "add_exported_type", + "add_exported_function", + "add_enum_variant", + "add_conformance_vector", +] +summary = "Add a pure deterministic NIP-09 evaluator that returns canonical suppression decisions and evidence for same-author event and inclusive address targets while preserving immutable events, kind-5 immunity, advisory-kind irrelevance, and storage ownership boundaries." diff --git a/crates/event_codec/README b/crates/event_codec/README @@ -109,7 +109,8 @@ valid shapes, diagnostics, exact and over-limit budgets, stable error precedence, and NIP-01 admission without secret keys, mutation recipes, or ambient state. -The Deletion codec implements the request-only boundary of +The Deletion codec implements the request and pure suppression-evaluation +boundaries of [NIP-09](https://github.com/nostr-protocol/nips/blob/bdfa7e62ef87fcfcb992b1a27aee49d36b0b4f91/09.md). `authored_nip09_deletion_request_to_wire_parts` emits canonical two-element `e` targets, then `a` targets, then the complete unique ascending set of `k` @@ -125,10 +126,23 @@ this boundary. `verify_and_admit_nip09_deletion_request_event` binds the projection to the verified request envelope. It does not authorize or apply a deletion, look up a target, compare authors, compute an address cutoff, suppress an event, or -mutate storage. The exact operation namespace is +mutate storage. + +`evaluate_nip09_suppression` is a separate pure operation over one +`RadrootsSignatureVerifiedEvent` candidate and admitted deletion requests. It +returns an explicit `RadrootsNip09SuppressionDecision` with canonical evidence +without changing any input or touching a store. Kind `5` is immune. Other +candidates require the request author to equal the candidate author. An exact +event-id target applies independently of timestamps; a canonical address +target applies through the inclusive maximum qualifying request timestamp, so +a later replacement is not suppressed. Advisory `k` values do not affect the +result, and request order cannot affect the decision. + +The exact operation namespace is `social.deletion_request.build_authored_draft`, -`social.deletion_request.project_verified_event`, and -`social.deletion_request.verify_and_admit_event`. Registry v7 makes +`social.deletion_request.project_verified_event`, +`social.deletion_request.verify_and_admit_event`, and +`social.deletion_request.evaluate_suppression`. Registry v7 makes `radroots.social.deletion_request.v1` `TypedOnly` for authoring and `AdmissionOnly` for matching, with generic kind-`5` authoring and publication reserved. @@ -141,6 +155,13 @@ cases cover canonical authoring, tolerant verified projection, diagnostics, exact and over-limit resource budgets, precedence, and admission without shipping keys, seeds, generators, or mutation recipes. +The separate +`contracts/conformance/vectors/deletion/suppression.v1.json` corpus and its +byte-identical packaged fixture execute the evaluator's same-author, direct +event, inclusive address-cutoff, later-replacement, pre-target request, +kind-`5` immunity, and deterministic evidence behavior. These vectors govern a +decision value only and claim no persistent store effect. + The FoodAvailability codec owns four boundaries for the focused `radroots.food.availability.v1` kind-`30402` profile. Strict details plus `created_at` produce bounded unsigned wire parts whose tag order is exactly diff --git a/crates/event_codec/src/deletion/evaluator.rs b/crates/event_codec/src/deletion/evaluator.rs @@ -0,0 +1,754 @@ +#[cfg(not(feature = "std"))] +use alloc::format; + +use radroots_event::{ + ids::{RadrootsEventId, RadrootsNip01Coordinate}, + kinds::KIND_DELETION_REQUEST, +}; + +use crate::{ + deletion::admission::RadrootsAdmittedNip09DeletionRequestEvent, + verification::RadrootsSignatureVerifiedEvent, +}; + +/// Whether a verified event remains visible after NIP-09 evaluation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsNip09SuppressionOutcome { + Visible, + Suppressed, +} + +impl RadrootsNip09SuppressionOutcome { + pub const fn code(self) -> &'static str { + match self { + Self::Visible => "visible", + Self::Suppressed => "suppressed", + } + } +} + +/// The stable explanation for a NIP-09 suppression outcome. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsNip09SuppressionReason { + DeletionRequestImmune, + NoAuthorizedReference, + RequestAuthorMismatch, + AddressCutoffPrecedesTarget, + EventIdReference, + AddressReferenceAtOrBeforeCutoff, + EventIdAndAddressReference, +} + +impl RadrootsNip09SuppressionReason { + pub const fn code(self) -> &'static str { + match self { + Self::DeletionRequestImmune => "deletion_request_immune", + Self::NoAuthorizedReference => "deletion_no_authorized_reference", + Self::RequestAuthorMismatch => "deletion_request_author_mismatch", + Self::AddressCutoffPrecedesTarget => "deletion_address_cutoff_precedes_target", + Self::EventIdReference => "deletion_event_id_reference", + Self::AddressReferenceAtOrBeforeCutoff => "deletion_address_reference", + Self::EventIdAndAddressReference => "deletion_event_id_and_address_reference", + } + } +} + +/// Canonical evidence for an authorized exact event-id reference. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsNip09EventReferenceEvidence { + request_id: RadrootsEventId, +} + +impl RadrootsNip09EventReferenceEvidence { + pub const fn request_id(&self) -> &RadrootsEventId { + &self.request_id + } +} + +/// Canonical evidence for authorized address references. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsNip09AddressReferenceEvidence { + coordinate: RadrootsNip01Coordinate, + inclusive_cutoff: u64, + request_id: RadrootsEventId, +} + +impl RadrootsNip09AddressReferenceEvidence { + pub const fn coordinate(&self) -> &RadrootsNip01Coordinate { + &self.coordinate + } + + pub const fn inclusive_cutoff(&self) -> u64 { + self.inclusive_cutoff + } + + pub const fn request_id(&self) -> &RadrootsEventId { + &self.request_id + } +} + +/// A pure NIP-09 visibility decision with canonical supporting evidence. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsNip09SuppressionDecision { + outcome: RadrootsNip09SuppressionOutcome, + reason: RadrootsNip09SuppressionReason, + event_reference: Option<RadrootsNip09EventReferenceEvidence>, + address_reference: Option<RadrootsNip09AddressReferenceEvidence>, +} + +impl RadrootsNip09SuppressionDecision { + pub const fn outcome(&self) -> RadrootsNip09SuppressionOutcome { + self.outcome + } + + pub const fn reason(&self) -> RadrootsNip09SuppressionReason { + self.reason + } + + pub const fn event_reference(&self) -> Option<&RadrootsNip09EventReferenceEvidence> { + self.event_reference.as_ref() + } + + pub const fn address_reference(&self) -> Option<&RadrootsNip09AddressReferenceEvidence> { + self.address_reference.as_ref() + } +} + +/// Evaluates deterministic NIP-09 suppression without mutating stored events. +pub fn evaluate_nip09_suppression( + target: &RadrootsSignatureVerifiedEvent, + requests: &[RadrootsAdmittedNip09DeletionRequestEvent], +) -> RadrootsNip09SuppressionDecision { + let target_event = target.event(); + if target_event.kind_u32() == KIND_DELETION_REQUEST { + return decision( + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::DeletionRequestImmune, + None, + None, + ); + } + + let target_coordinate = nip01_coordinate(target); + let mut event_reference = None; + let mut address_reference = None; + let mut has_unauthorized_reference = false; + + for request in requests { + let request_event = request.event(); + let projection = request.projection(); + let event_matches = projection + .event_targets() + .iter() + .any(|reference| reference.event_id() == target_event.id()); + let address_match = target_coordinate.as_ref().filter(|coordinate| { + projection + .address_targets() + .iter() + .any(|reference| reference.coordinate() == *coordinate) + }); + if !event_matches && address_match.is_none() { + continue; + } + if request_event.author() != target_event.author() { + has_unauthorized_reference = true; + continue; + } + + if event_matches + && event_reference.as_ref().is_none_or( + |current: &RadrootsNip09EventReferenceEvidence| { + request_event.id() < current.request_id() + }, + ) + { + event_reference = Some(RadrootsNip09EventReferenceEvidence { + request_id: request_event.id().clone(), + }); + } + if let Some(coordinate) = address_match { + let inclusive_cutoff = request_event.created_at_u64(); + if address_reference.as_ref().is_none_or( + |current: &RadrootsNip09AddressReferenceEvidence| { + inclusive_cutoff > current.inclusive_cutoff() + || (inclusive_cutoff == current.inclusive_cutoff() + && request_event.id() < current.request_id()) + }, + ) { + address_reference = Some(RadrootsNip09AddressReferenceEvidence { + coordinate: coordinate.clone(), + inclusive_cutoff, + request_id: request_event.id().clone(), + }); + } + } + } + + let address_applies = address_reference + .as_ref() + .is_some_and(|reference| target_event.created_at_u64() <= reference.inclusive_cutoff()); + let (outcome, reason) = match (event_reference.is_some(), address_applies) { + (true, true) => ( + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::EventIdAndAddressReference, + ), + (true, false) => ( + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::EventIdReference, + ), + (false, true) => ( + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff, + ), + (false, false) if address_reference.is_some() => ( + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget, + ), + (false, false) if has_unauthorized_reference => ( + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::RequestAuthorMismatch, + ), + (false, false) => ( + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::NoAuthorizedReference, + ), + }; + + decision(outcome, reason, event_reference, address_reference) +} + +fn nip01_coordinate(target: &RadrootsSignatureVerifiedEvent) -> Option<RadrootsNip01Coordinate> { + let event = target.event(); + let kind = event.kind_u32(); + let identifier = if matches!(kind, 0 | 3) || (10_000..=19_999).contains(&kind) { + "" + } else if (30_000..=39_999).contains(&kind) { + event + .tag_slices() + .iter() + .find(|tag| tag.as_slice().first().is_some_and(|name| name == "d"))? + .as_slice() + .get(1)? + .as_str() + } else { + return None; + }; + RadrootsNip01Coordinate::parse(format!("{kind}:{}:{identifier}", event.author().as_str())).ok() +} + +const fn decision( + outcome: RadrootsNip09SuppressionOutcome, + reason: RadrootsNip09SuppressionReason, + event_reference: Option<RadrootsNip09EventReferenceEvidence>, + address_reference: Option<RadrootsNip09AddressReferenceEvidence>, +) -> RadrootsNip09SuppressionDecision { + RadrootsNip09SuppressionDecision { + outcome, + reason, + event_reference, + address_reference, + } +} + +#[cfg(all(test, feature = "nostr"))] +mod tests { + use super::*; + + use nostr::{Keys, SECP256K1, secp256k1::Message}; + use radroots_event::{ + RadrootsEventEnvelope, RadrootsEventEnvelopeParts, wire::compute_canonical_nip01_event_id, + }; + + use crate::{ + deletion::admission::verify_and_admit_nip09_deletion_request_event, + test_fixtures::{FIXTURE_ALICE_SECRET_KEY_HEX, FIXTURE_BOB_SECRET_KEY_HEX}, + verification::verify_nip01_event, + }; + + const TARGET_TIME: u64 = 1_800_100_100; + + #[test] + fn nip09_evaluator_codes_and_opaque_evidence_are_stable() { + let outcome_codes = [ + (RadrootsNip09SuppressionOutcome::Visible, "visible"), + (RadrootsNip09SuppressionOutcome::Suppressed, "suppressed"), + ]; + for (outcome, code) in outcome_codes { + assert_eq!(outcome.code(), code); + } + + let reason_codes = [ + ( + RadrootsNip09SuppressionReason::DeletionRequestImmune, + "deletion_request_immune", + ), + ( + RadrootsNip09SuppressionReason::NoAuthorizedReference, + "deletion_no_authorized_reference", + ), + ( + RadrootsNip09SuppressionReason::RequestAuthorMismatch, + "deletion_request_author_mismatch", + ), + ( + RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget, + "deletion_address_cutoff_precedes_target", + ), + ( + RadrootsNip09SuppressionReason::EventIdReference, + "deletion_event_id_reference", + ), + ( + RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff, + "deletion_address_reference", + ), + ( + RadrootsNip09SuppressionReason::EventIdAndAddressReference, + "deletion_event_id_and_address_reference", + ), + ]; + for (reason, code) in reason_codes { + assert_eq!(reason.code(), code); + } + } + + #[test] + fn nip09_evaluator_deletion_requests_are_immune() { + let target = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME, + vec![event_reference("a")], + "target", + ); + let request = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME + 1, + vec![event_reference(target.event().id_str())], + "attempt", + ); + + let decision = evaluate_nip09_suppression(target.verified_event(), &[request]); + + assert_decision( + &decision, + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::DeletionRequestImmune, + ); + assert!(decision.event_reference().is_none()); + assert!(decision.address_reference().is_none()); + } + + #[test] + fn nip09_evaluator_exact_event_reference_is_time_independent() { + let target = verified_event( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME, + 1, + Vec::new(), + "target", + ); + let request = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME - 1, + vec![event_reference(target.event().id_str())], + "exact", + ); + let request_id = request.event().id().clone(); + + let decision = evaluate_nip09_suppression(&target, &[request]); + + assert_decision( + &decision, + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::EventIdReference, + ); + assert_eq!( + decision + .event_reference() + .expect("event evidence") + .request_id(), + &request_id + ); + assert!(decision.address_reference().is_none()); + } + + #[test] + fn nip09_evaluator_address_cutoff_is_inclusive_and_later_replacement_is_visible() { + let at_cutoff = verified_event( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME, + 30_402, + vec![d_tag("produce")], + "at cutoff", + ); + let coordinate = coordinate_for(&at_cutoff, "produce"); + let request = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME, + vec![address_reference(coordinate.as_str())], + "address", + ); + let request_id = request.event().id().clone(); + + let suppressed = evaluate_nip09_suppression(&at_cutoff, core::slice::from_ref(&request)); + assert_decision( + &suppressed, + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff, + ); + let evidence = suppressed.address_reference().expect("address evidence"); + assert_eq!(evidence.coordinate(), &coordinate); + assert_eq!(evidence.inclusive_cutoff(), TARGET_TIME); + assert_eq!(evidence.request_id(), &request_id); + + let later = verified_event( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME + 1, + 30_402, + vec![d_tag("produce")], + "later", + ); + let visible = evaluate_nip09_suppression(&later, &[request]); + assert_decision( + &visible, + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget, + ); + assert_eq!( + visible + .address_reference() + .expect("stale address evidence") + .inclusive_cutoff(), + TARGET_TIME + ); + } + + #[test] + fn nip09_evaluator_author_mismatch_and_unrelated_requests_are_distinct() { + let target = verified_event( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME, + 1, + Vec::new(), + "target", + ); + let mismatch = admitted_request( + FIXTURE_BOB_SECRET_KEY_HEX, + TARGET_TIME + 1, + vec![event_reference(target.event().id_str())], + "wrong author", + ); + let unrelated = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME + 1, + vec![event_reference("f")], + "unrelated", + ); + + let mismatch_decision = evaluate_nip09_suppression(&target, &[mismatch]); + assert_decision( + &mismatch_decision, + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::RequestAuthorMismatch, + ); + + let unrelated_decision = evaluate_nip09_suppression(&target, &[unrelated]); + assert_decision( + &unrelated_decision, + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::NoAuthorizedReference, + ); + } + + #[test] + fn nip09_evaluator_authorized_stale_address_precedes_unauthorized_exact_event() { + let target = verified_event( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME, + 0, + Vec::new(), + "profile", + ); + let coordinate = coordinate_for(&target, ""); + let stale = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME - 1, + vec![address_reference(coordinate.as_str())], + "stale", + ); + let mismatch = admitted_request( + FIXTURE_BOB_SECRET_KEY_HEX, + TARGET_TIME + 1, + vec![event_reference(target.event().id_str())], + "wrong author", + ); + + let decision = evaluate_nip09_suppression(&target, &[mismatch, stale]); + + assert_decision( + &decision, + RadrootsNip09SuppressionOutcome::Visible, + RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget, + ); + } + + #[test] + fn nip09_evaluator_exact_event_dominates_stale_address_reference() { + let target = verified_event( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME, + 30_000, + vec![d_tag("harvest")], + "target", + ); + let coordinate = coordinate_for(&target, "harvest"); + let exact = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME - 2, + vec![event_reference(target.event().id_str())], + "exact", + ); + let stale = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME - 1, + vec![address_reference(coordinate.as_str())], + "stale", + ); + + let decision = evaluate_nip09_suppression(&target, &[stale, exact]); + + assert_decision( + &decision, + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::EventIdReference, + ); + assert!(decision.event_reference().is_some()); + assert!(decision.address_reference().is_some()); + } + + #[test] + fn nip09_evaluator_reduction_is_order_and_repeat_invariant() { + let target = verified_event( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME, + 39_999, + vec![d_tag("market"), d_tag("ignored")], + "target", + ); + let coordinate = coordinate_for(&target, "market"); + let exact_a = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME - 10, + vec![event_reference(target.event().id_str())], + "exact a", + ); + let exact_b = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME + 20, + vec![event_reference(target.event().id_str())], + "exact b", + ); + let address_a = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME + 20, + vec![address_reference(coordinate.as_str())], + "address a", + ); + let address_b = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME + 20, + vec![address_reference(coordinate.as_str())], + "address b", + ); + let address_older = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME + 10, + vec![address_reference(coordinate.as_str())], + "address older", + ); + let lower_exact_id = core::cmp::min(exact_a.event().id(), exact_b.event().id()).clone(); + let lower_address_id = + core::cmp::min(address_a.event().id(), address_b.event().id()).clone(); + + let forward = vec![ + exact_a.clone(), + address_a.clone(), + exact_b.clone(), + address_b.clone(), + address_older.clone(), + ]; + let repeated_reverse = vec![ + address_b, + address_older, + exact_b, + address_a, + exact_a.clone(), + exact_a, + ]; + let expected = evaluate_nip09_suppression(&target, &forward); + let actual = evaluate_nip09_suppression(&target, &repeated_reverse); + + assert_eq!(actual, expected); + assert_decision( + &actual, + RadrootsNip09SuppressionOutcome::Suppressed, + RadrootsNip09SuppressionReason::EventIdAndAddressReference, + ); + assert_eq!( + actual + .event_reference() + .expect("event evidence") + .request_id(), + &lower_exact_id + ); + let address = actual.address_reference().expect("address evidence"); + assert_eq!(address.coordinate(), &coordinate); + assert_eq!(address.inclusive_cutoff(), TARGET_TIME + 20); + assert_eq!(address.request_id(), &lower_address_id); + } + + #[test] + fn nip09_evaluator_uses_generic_coordinates_and_rejects_malformed_first_d() { + for kind in [0, 3, 10_000, 19_999] { + let target = verified_event( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME, + kind, + Vec::new(), + "replaceable", + ); + let coordinate = coordinate_for(&target, ""); + let request = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME, + vec![address_reference(coordinate.as_str())], + "replaceable", + ); + assert_eq!( + evaluate_nip09_suppression(&target, &[request]).reason(), + RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff + ); + } + + let target = verified_event( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME, + 30_000, + vec![vec!["d".to_string()], d_tag("later")], + "malformed", + ); + let later_coordinate = coordinate_for(&target, "later"); + let request = admitted_request( + FIXTURE_ALICE_SECRET_KEY_HEX, + TARGET_TIME, + vec![address_reference(later_coordinate.as_str())], + "must not match", + ); + assert_eq!( + evaluate_nip09_suppression(&target, &[request]).reason(), + RadrootsNip09SuppressionReason::NoAuthorizedReference + ); + } + + fn assert_decision( + decision: &RadrootsNip09SuppressionDecision, + outcome: RadrootsNip09SuppressionOutcome, + reason: RadrootsNip09SuppressionReason, + ) { + assert_eq!(decision.outcome(), outcome); + assert_eq!(decision.reason(), reason); + } + + fn event_reference(value: &str) -> Vec<String> { + vec!["e".to_string(), normalize_event_id(value)] + } + + fn address_reference(value: &str) -> Vec<String> { + vec!["a".to_string(), value.to_string()] + } + + fn d_tag(value: &str) -> Vec<String> { + vec!["d".to_string(), value.to_string()] + } + + fn normalize_event_id(value: &str) -> String { + if value.len() == 1 { + value.repeat(64) + } else { + value.to_string() + } + } + + fn coordinate_for( + target: &RadrootsSignatureVerifiedEvent, + identifier: &str, + ) -> RadrootsNip01Coordinate { + RadrootsNip01Coordinate::parse(format!( + "{}:{}:{identifier}", + target.event().kind_u32(), + target.event().author_str() + )) + .expect("target coordinate") + } + + fn admitted_request( + secret_key_hex: &str, + created_at: u64, + tags: Vec<Vec<String>>, + content: &str, + ) -> RadrootsAdmittedNip09DeletionRequestEvent { + verify_and_admit_nip09_deletion_request_event(signed_event( + secret_key_hex, + created_at, + KIND_DELETION_REQUEST, + tags, + content, + )) + .expect("valid admitted deletion request") + } + + fn verified_event( + secret_key_hex: &str, + created_at: u64, + kind: u32, + tags: Vec<Vec<String>>, + content: &str, + ) -> RadrootsSignatureVerifiedEvent { + verify_nip01_event(signed_event( + secret_key_hex, + created_at, + kind, + tags, + content, + )) + .expect("valid verified event") + } + + fn signed_event( + secret_key_hex: &str, + created_at: u64, + kind: u32, + tags: Vec<Vec<String>>, + content: &str, + ) -> RadrootsEventEnvelope { + let keys = Keys::parse(secret_key_hex).expect("fixed fixture secret key must parse"); + let author = keys.public_key().to_string(); + let id = + compute_canonical_nip01_event_id(author.as_str(), created_at, kind, &tags, content) + .expect("canonical event id"); + let nostr_id = nostr::EventId::from_hex(id.as_str()).expect("Nostr event id"); + let message = Message::from_digest(nostr_id.to_bytes()); + let signature = SECP256K1.sign_schnorr_no_aux_rand(&message, keys.key_pair(SECP256K1)); + + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: id.into_string(), + author, + created_at, + kind, + tags, + content: content.to_string(), + sig: signature.to_string(), + }) + .expect("valid event envelope") + } +} diff --git a/crates/event_codec/src/deletion/mod.rs b/crates/event_codec/src/deletion/mod.rs @@ -1,3 +1,4 @@ pub mod admission; pub mod authored; +pub mod evaluator; pub mod inbound; diff --git a/crates/event_codec/tests/fixtures/deletion_suppression.v1.json b/crates/event_codec/tests/fixtures/deletion_suppression.v1.json @@ -0,0 +1,609 @@ +{ + "suite": "nip09_suppression_evaluator", + "contract_version": "1.0.0", + "vectors": [ + { + "id": "nip09_suppress_no_requests_visible", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_no_authorized_reference" + } + }, + { + "id": "nip09_suppress_same_author_event_reference", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"fade0f4ddcd9f2f8b9c6508055ceb58c687bd8b99cdaf5f4b63cc13b16f75278\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"event reference a\",\"sig\":\"36ab6c25bcc0fa22da55a566121cf43210f90a8f0f9243736fd24ff57b4799ac09cf017ef3cd132464b728bb5ff87603535fbb1b566413ed5f34a60c1838a809\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": { + "request_id": "fade0f4ddcd9f2f8b9c6508055ceb58c687bd8b99cdaf5f4b63cc13b16f75278" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + }, + { + "id": "nip09_suppress_same_author_nonmatching_reference", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"9b3d3a1ca7f4a7019a17c86465cadfeaa262ebc76042d2a5a07971e09b36d14c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"120bd6d1a82eb11b24f05316f3254c0fa6cb87fcc909d7200a963e40207f5e77\"]],\"content\":\"other reference\",\"sig\":\"9765abc98be250a2b60b6f3aa33e695ff58e0aa5f5b2e927cbb878acdb25b645a8d1a9d03f6cb9de69448c4ec6bc7e6f2cc5276a3865ac0a4d4c8daf4b331a48\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_no_authorized_reference" + } + }, + { + "id": "nip09_suppress_wrong_author_exact_event_reference", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"403981275c07a7c67d35552ee3dbc416c20b770d5485216ff2d1b18ad287588b\",\"pubkey\":\"e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"wrong author event\",\"sig\":\"e7f2e84135c979c0d3fa815d3b9082163152e37d55cf0f40e21e4df81baee81efe1365b0011da940b63efb7dc9d13fb7d84d990aee4a04e4dd582790ff205770\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_request_author_mismatch" + } + }, + { + "id": "nip09_suppress_event_reference_predates_target", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"a75e12b2175feafcad0763545cbbf31c118915f60d97572bb68fafbcc533f6be\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100190,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"pre-target exact event\",\"sig\":\"c2fbe879659b5da522dbb400ac08174f522482a8077fa10c868c1a395598e2e40175d1f232964d897f84e9e710de8c9a2b0feff088cead33a5d1879a96fa1d7d\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": { + "request_id": "a75e12b2175feafcad0763545cbbf31c118915f60d97572bb68fafbcc533f6be" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + }, + { + "id": "nip09_suppress_deletion_request_immune_event_reference", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"a8fb77cd006005bffaa04f49ba01560dccfc2ca9875d79bd0a7bdc3862553df5\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"a9bbfa0344854ced08a13230a3a84b2d3971f5bf08f1f6af25f1547eec307a89\"]],\"content\":\"delete a deletion\",\"sig\":\"52a7356d38a5a49c32094905a5b4ff5c4adb41146a6b046377541cd91cf44b2f0cdb7640aab556c8375fafc8460f6f3076731d12b9923cc15538d54dcdd47f6a\"}" + ], + "target_event_json": "{\"id\":\"a9bbfa0344854ced08a13230a3a84b2d3971f5bf08f1f6af25f1547eec307a89\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":5,\"tags\":[[\"e\",\"120bd6d1a82eb11b24f05316f3254c0fa6cb87fcc909d7200a963e40207f5e77\"]],\"content\":\"original deletion\",\"sig\":\"5f17a714f308ca33c2d5f1e4b52d2a56a406474ae0e6a1cd4482b3a3cf116fdab7cebe77e5a6cc18a2490f32292bcdf4df08ab8d3a2d507df4cd99c39e6fb1f7\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_request_immune" + } + }, + { + "id": "nip09_suppress_deletion_request_immune_mixed_references", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"6cebea08ac79c820dac9936444a818dd8aa89dffb5b0b251b21edea266463860\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100211,\"kind\":5,\"tags\":[[\"e\",\"a9bbfa0344854ced08a13230a3a84b2d3971f5bf08f1f6af25f1547eec307a89\"],[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"mixed deletion target\",\"sig\":\"464b930b47df90bf5c9d6f0138c082d71c3fb84f49755168e690672cc1a47635c507518f876cc9aceff60765c3db9a8be1b774a3322b79dbe82bd45f3c06de8b\"}" + ], + "target_event_json": "{\"id\":\"a9bbfa0344854ced08a13230a3a84b2d3971f5bf08f1f6af25f1547eec307a89\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":5,\"tags\":[[\"e\",\"120bd6d1a82eb11b24f05316f3254c0fa6cb87fcc909d7200a963e40207f5e77\"]],\"content\":\"original deletion\",\"sig\":\"5f17a714f308ca33c2d5f1e4b52d2a56a406474ae0e6a1cd4482b3a3cf116fdab7cebe77e5a6cc18a2490f32292bcdf4df08ab8d3a2d507df4cd99c39e6fb1f7\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_request_immune" + } + }, + { + "id": "nip09_suppress_address_cutoff_before_target", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"1473dabefc767c7a2a16ce716e0cbcd1ea463299c6e1e4d1c805672669012bd2\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100199,\"kind\":5,\"tags\":[[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"address before\",\"sig\":\"7bdf964d33f45eaeb81fbc671ae994dfdcf61dcac6c37f70831be76148ff9e05a2a0abe673a8655a2d3acaffdfba5aaa719cc00a26b9f3938626839681e991dd\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100199, + "request_id": "1473dabefc767c7a2a16ce716e0cbcd1ea463299c6e1e4d1c805672669012bd2" + }, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_address_cutoff_precedes_target" + } + }, + { + "id": "nip09_suppress_address_cutoff_equal_target", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"3d75aa21ad472076c331419a3f2e73284ef8083f79d11f47b4c79e76604dc775\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":5,\"tags\":[[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"address equal\",\"sig\":\"aeda08b49690df9655012e964b69dca4c9f5f1545423378581875b8fb1ea27719ee0ba1c6c54a06c6938dec8b967379403c45d43e208b6746715f0cdf8c26e73\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100200, + "request_id": "3d75aa21ad472076c331419a3f2e73284ef8083f79d11f47b4c79e76604dc775" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_address_cutoff_after_target", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"086915e7b1c08ba14f5c8fabca8490b56b022b093ce21490d8f706df83c13e2c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100201,\"kind\":5,\"tags\":[[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"address after\",\"sig\":\"49dc8bd554da237d73ac266f893895df0509e4cf3b5eb372b393c42b8872bf1afb1d1922be622acbf6b811657d15beac4f6a2e69d24206b185db3135ed6cdb70\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100201, + "request_id": "086915e7b1c08ba14f5c8fabca8490b56b022b093ce21490d8f706df83c13e2c" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_address_wrong_kind", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"b24aa726605e907b6d2733e8e36e47f890e544583c976fa0347b054d706e74ec\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"3:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"wrong kind\",\"sig\":\"a03a7de08ba411613602fd0d8d04f70dbe81dd194bb7a556691595a2830a0334883cb0b45d9e6ddb253aed25de405da567c4c9f48a2064172ddef88959b7209c\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_no_authorized_reference" + } + }, + { + "id": "nip09_suppress_address_wrong_identifier", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"8388ea59c1f08f5091b3332e172145ef09bc671e7ad71e44c198867a66578824\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"39999:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:orchard:east/plot\"]],\"content\":\"wrong identifier\",\"sig\":\"85f06ad83418d262150642d1b64eea745f410232c6589d7cde7fd63fd896fd66b644db93a275cbeceadebf0f4976ee9cdee38ef2e6f84c76364e9309a4c614bb\"}" + ], + "target_event_json": "{\"id\":\"198aeb65fa856b3eb7b5a7fe36fa90da70fbdbb1cb88b31a3893d07c9437c796\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":39999,\"tags\":[[\"d\",\"orchard:west/plot\"]],\"content\":\"opaque addressable\",\"sig\":\"cc3db13caa8e407e842d5a977f720624a17364bf617179aa5a9318e7ab111818b82d20799e074092522fa49c8b709e4b8af1877a0599c63797f81a71689b9638\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_no_authorized_reference" + } + }, + { + "id": "nip09_suppress_address_wrong_pubkey", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"39ab140373199234d51f13034f0383a6d7a492d49303cf0b5c08f31d876674f6\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"0:e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af:\"]],\"content\":\"wrong pubkey\",\"sig\":\"efc60d415f344ce7103ad9179a6642c5823caf2e4b44f642276130f8be7e9400d882ce1299bdf898bdbfc01efd105f525c2a3dc55dd0d56531b74ed173b171a6\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_no_authorized_reference" + } + }, + { + "id": "nip09_suppress_wrong_author_address_reference", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"616960881e5bdaa21bff166269193e8503fc0d8f55507ec955a55421c34c46c5\",\"pubkey\":\"e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"wrong author address\",\"sig\":\"515d51aca57cecaeb94111daf508979c6b78523ef4005ffed9966a39beeaea26c01b727482f82ddea88db8f3333f4e00dd6fa1ad4eddd0ec43ba213bb44f96f5\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": null, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_request_author_mismatch" + } + }, + { + "id": "nip09_suppress_replaceable_kind_0", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"086915e7b1c08ba14f5c8fabca8490b56b022b093ce21490d8f706df83c13e2c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100201,\"kind\":5,\"tags\":[[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"address after\",\"sig\":\"49dc8bd554da237d73ac266f893895df0509e4cf3b5eb372b393c42b8872bf1afb1d1922be622acbf6b811657d15beac4f6a2e69d24206b185db3135ed6cdb70\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100201, + "request_id": "086915e7b1c08ba14f5c8fabca8490b56b022b093ce21490d8f706df83c13e2c" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_replaceable_kind_3", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"e10db54031bbff5da53d70d7bba7f6ee3ce4497152618cad9e2ea78567874169\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"3:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"replaceable kind 3\",\"sig\":\"816d45c35ebdef5495ec1060f52fad19ea88b4d093431e1ae194ca5e7380145950b2b03dea2dd701e13a4e99c0c6071bb2e45e59782ce3a0547ef8a4ff6dadab\"}" + ], + "target_event_json": "{\"id\":\"9b7a698850a451d198e792be95a10bb093ccfd65ebbdc8395d4282c930f281ae\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":3,\"tags\":[],\"content\":\"contacts\",\"sig\":\"30e46ce6dc57368992a86e2eace20470164de87aeecc08c9717231af4b75eb45e6945fc3b07be3cbf640a2b7fb75c9811903f94027bbe39374cc1a06155a4aea\"}" + }, + "expected": { + "address_reference": { + "coordinate": "3:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100220, + "request_id": "e10db54031bbff5da53d70d7bba7f6ee3ce4497152618cad9e2ea78567874169" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_replaceable_kind_10000", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"e512278a22f532facc56739af02486b579a680b7a71ab0e064b86875049f79b1\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"10000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"replaceable kind 10000\",\"sig\":\"b814886b0f702015d37ed545e14771ae9c712ca57ad7e81437add6e109060d346b1695c7abb6b1810198d89af6fe0a50ee5eb54ed4676024652276932ea0d4de\"}" + ], + "target_event_json": "{\"id\":\"db73709e2333601fa97d8ade507c1fb046d37129d32b99e4ff24b316895399e1\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":10000,\"tags\":[],\"content\":\"replaceable lower\",\"sig\":\"8152e75fefbfa5e361402a70c4ee796d5cc6e11cf98ea447e2e7f53a8ad6b8618edea1dcdd8194deaf2304c5e53b442f56fc9e8809b062d0f6a1f07ea1caddb2\"}" + }, + "expected": { + "address_reference": { + "coordinate": "10000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100220, + "request_id": "e512278a22f532facc56739af02486b579a680b7a71ab0e064b86875049f79b1" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_replaceable_kind_19999", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"9bafe3e4a5ea4bc2f6c2027c096bab9962949d0a172003a3177f7739dc1e55eb\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"19999:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"replaceable kind 19999\",\"sig\":\"8d8ecdcf165819bff0f173289a8ad13758b812f36a12b43de37f1cea468a893e89ae542121b6e3d3fb75b52a946ef2ac81dfad209350ece01b4548d4a04a61cc\"}" + ], + "target_event_json": "{\"id\":\"22ae552d598bd60490890db49e4738fc6d10376dc9fb9765fb319b3d3d72a472\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":19999,\"tags\":[],\"content\":\"replaceable upper\",\"sig\":\"16db6c2afd5eaf97143d0ecf1ab6535c2df08e1d61dffcf2feabea54dca1a42d3f11b1d82ac77bb60b4498469a25823c1adc6ae133a00ddf77c86ba6f6ccbb60\"}" + }, + "expected": { + "address_reference": { + "coordinate": "19999:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100220, + "request_id": "9bafe3e4a5ea4bc2f6c2027c096bab9962949d0a172003a3177f7739dc1e55eb" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_addressable_kind_30000_empty_identifier", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"9900ec21cbddfe6598f64983cf52232ff3b59250fa41810dc8ca883a22c21d49\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"addressable kind 30000\",\"sig\":\"a8cee6e2fe394cf26ac783a57d275a8891f716cc8077a85e488fd02d04c9c821ce65e98543b022b1cf62733d2e7a0fbfdfaaec2c49bad68f761d49dfd09ad262\"}" + ], + "target_event_json": "{\"id\":\"8cb1b211dc3b8bfbd7f457ffe8f4b067f0b065c2658126ee65a82f43ffb67c5a\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable initial\",\"sig\":\"a60cffce8efd7b1a1372d6d0b175afc49bcd1e7a88b94f3e57ef212a4b7f9dbbb91c058c2af11209a20b2cd30d6ce74886fcf770c726dc8c3e20d0081e0ec1fa\"}" + }, + "expected": { + "address_reference": { + "coordinate": "30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100220, + "request_id": "9900ec21cbddfe6598f64983cf52232ff3b59250fa41810dc8ca883a22c21d49" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_addressable_kind_39999_opaque_identifier", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"9a308e463030f356e68abefba2bc9c766c766f509ec6809c8709ebe7da8d355e\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"a\",\"39999:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:orchard:west/plot\"]],\"content\":\"addressable kind 39999\",\"sig\":\"bd7b5f63aaf54a80283da21103556e235851b9e31e5b668808ceff8f6d1af303b4334b7855478b277bca7da1775a630b46671ca195afd1f920752a7cb391afb2\"}" + ], + "target_event_json": "{\"id\":\"198aeb65fa856b3eb7b5a7fe36fa90da70fbdbb1cb88b31a3893d07c9437c796\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":39999,\"tags\":[[\"d\",\"orchard:west/plot\"]],\"content\":\"opaque addressable\",\"sig\":\"cc3db13caa8e407e842d5a977f720624a17364bf617179aa5a9318e7ab111818b82d20799e074092522fa49c8b709e4b8af1877a0599c63797f81a71689b9638\"}" + }, + "expected": { + "address_reference": { + "coordinate": "39999:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:orchard:west/plot", + "inclusive_cutoff": 1800100220, + "request_id": "9a308e463030f356e68abefba2bc9c766c766f509ec6809c8709ebe7da8d355e" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_combined_event_and_address_references", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"eec90d8cb7e493d68b197310454c1f5b92828af5973aa26ee8d5fb82e0f30434\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"e\",\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\"],[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"combined references\",\"sig\":\"3f8203c5d57289e9ebdc5c2e2ab7f487ee60e409d5d4667057eba0ad10153c59aa8236d37e7d177a693243f9c9896cf05f8afd971dfc358438a55cc3ff5ebfaf\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100220, + "request_id": "eec90d8cb7e493d68b197310454c1f5b92828af5973aa26ee8d5fb82e0f30434" + }, + "event_reference": { + "request_id": "eec90d8cb7e493d68b197310454c1f5b92828af5973aa26ee8d5fb82e0f30434" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_and_address_reference" + } + }, + { + "id": "nip09_suppress_event_reference_with_stale_address", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"a0165c75548878cd6a959ab2a12e3826c1b118240c328cc0470d441b7306edf0\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100199,\"kind\":5,\"tags\":[[\"e\",\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\"],[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"exact event stale address\",\"sig\":\"512a96994d5f357abd9cd9c7cc7a00658ec4ee7cbe4bb6d83fbe26b22c75755f92c43f89b4ff8f12ce0c06e44fa1277c370aac1de4551543831a6640cb1496f4\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100199, + "request_id": "a0165c75548878cd6a959ab2a12e3826c1b118240c328cc0470d441b7306edf0" + }, + "event_reference": { + "request_id": "a0165c75548878cd6a959ab2a12e3826c1b118240c328cc0470d441b7306edf0" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + }, + { + "id": "nip09_suppress_kind_advisory_diagnostics_ignored", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"859689351e1457702ea9fb97e056185781bd779dc3889b42b9bed49936691ea9\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"],[\"k\"],[\"k\",\"bogus\"],[\"k\",\"1\"],[\"k\",\"1\"],[\"k\",\"2\"]],\"content\":\"ignored kind diagnostics\",\"sig\":\"5dfaa28b172be180313aee8e204b08a2a4a208f81ecdd1727f0d55bddfd57fbd62ac5cc0cbda5f17f8f4bd4bf758f77c0685723859d9269e0a2c7f39bde88354\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": { + "request_id": "859689351e1457702ea9fb97e056185781bd779dc3889b42b9bed49936691ea9" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + }, + { + "id": "nip09_suppress_duplicate_raw_targets_deduplicated", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"ab329bb4dcb4e7b3b706f44b1212e8464d0c1a622e0f7d4cb5da62b5b7291ade\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100220,\"kind\":5,\"tags\":[[\"e\",\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\"],[\"e\",\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\"],[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"],[\"a\",\"0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"duplicate raw references\",\"sig\":\"7479c3b44811e401d3cf51140779db9b2b9c6b04194f643e01c4b5f2f3adfa54812ebb1f36d83cdc5d8dcc72eb88335fae281c9383135dbe7da41c0e5a1287fe\"}" + ], + "target_event_json": "{\"id\":\"8b1da693935771675c95b8a47c883f03edcc96060d4cbeaa07280e7b2b505a35\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":0,\"tags\":[],\"content\":\"profile zero\",\"sig\":\"b6c8075b9e14f9397b5c51a7ccc4fe83971a14144801c86153c45e2455dc82fa921f66af12f1d2e99fccab2608b22b8d4d6920e81f965073ac65762b065fa926\"}" + }, + "expected": { + "address_reference": { + "coordinate": "0:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100220, + "request_id": "ab329bb4dcb4e7b3b706f44b1212e8464d0c1a622e0f7d4cb5da62b5b7291ade" + }, + "event_reference": { + "request_id": "ab329bb4dcb4e7b3b706f44b1212e8464d0c1a622e0f7d4cb5da62b5b7291ade" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_and_address_reference" + } + }, + { + "id": "nip09_suppress_max_address_cutoff_forward_order", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"88979696e9bbc7762dd224aa4a7c95dbe19567e0d83667185472a4124105c7aa\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100230,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 230\",\"sig\":\"420f8775b6e5454e8fb69207f00b1c9d215e3f3f837e3433d1c939acba5bb7dcea64db45edcfbeb3c621696cf9c58ecb469e298a2377ca28c399a8e2c5dd97bd\"}", + "{\"id\":\"2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100240,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 240 a\",\"sig\":\"7562e4477358a0788f7ca4052944fb647235910504de38280b3737b0e43cb1628beb4a19312d83e93438a131d9ab7239193696bc61d70e09e370c98023c45cd2\"}" + ], + "target_event_json": "{\"id\":\"8cb1b211dc3b8bfbd7f457ffe8f4b067f0b065c2658126ee65a82f43ffb67c5a\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable initial\",\"sig\":\"a60cffce8efd7b1a1372d6d0b175afc49bcd1e7a88b94f3e57ef212a4b7f9dbbb91c058c2af11209a20b2cd30d6ce74886fcf770c726dc8c3e20d0081e0ec1fa\"}" + }, + "expected": { + "address_reference": { + "coordinate": "30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100240, + "request_id": "2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_max_address_cutoff_reverse_order", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100240,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 240 a\",\"sig\":\"7562e4477358a0788f7ca4052944fb647235910504de38280b3737b0e43cb1628beb4a19312d83e93438a131d9ab7239193696bc61d70e09e370c98023c45cd2\"}", + "{\"id\":\"88979696e9bbc7762dd224aa4a7c95dbe19567e0d83667185472a4124105c7aa\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100230,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 230\",\"sig\":\"420f8775b6e5454e8fb69207f00b1c9d215e3f3f837e3433d1c939acba5bb7dcea64db45edcfbeb3c621696cf9c58ecb469e298a2377ca28c399a8e2c5dd97bd\"}" + ], + "target_event_json": "{\"id\":\"8cb1b211dc3b8bfbd7f457ffe8f4b067f0b065c2658126ee65a82f43ffb67c5a\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable initial\",\"sig\":\"a60cffce8efd7b1a1372d6d0b175afc49bcd1e7a88b94f3e57ef212a4b7f9dbbb91c058c2af11209a20b2cd30d6ce74886fcf770c726dc8c3e20d0081e0ec1fa\"}" + }, + "expected": { + "address_reference": { + "coordinate": "30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100240, + "request_id": "2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_later_revision_survives_address_cutoff", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"88979696e9bbc7762dd224aa4a7c95dbe19567e0d83667185472a4124105c7aa\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100230,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 230\",\"sig\":\"420f8775b6e5454e8fb69207f00b1c9d215e3f3f837e3433d1c939acba5bb7dcea64db45edcfbeb3c621696cf9c58ecb469e298a2377ca28c399a8e2c5dd97bd\"}", + "{\"id\":\"2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100240,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 240 a\",\"sig\":\"7562e4477358a0788f7ca4052944fb647235910504de38280b3737b0e43cb1628beb4a19312d83e93438a131d9ab7239193696bc61d70e09e370c98023c45cd2\"}" + ], + "target_event_json": "{\"id\":\"d35339c16ecb0baff3149324c96c01983e4263278a1e1d6f58ad41f538bcd3ed\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100260,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable later\",\"sig\":\"55e1bffad1fa0553c1b67f27a15f226c3ea18ba2a2716f12a8ae46cdd007d2c3b2a6d36997521de8a70cc2914dceb1f0fe1664176e27c458069f7c04b9383ad4\"}" + }, + "expected": { + "address_reference": { + "coordinate": "30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100240, + "request_id": "2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c" + }, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_address_cutoff_precedes_target" + } + }, + { + "id": "nip09_suppress_equal_max_cutoff_uses_lowest_request_id", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100240,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 240 a\",\"sig\":\"7562e4477358a0788f7ca4052944fb647235910504de38280b3737b0e43cb1628beb4a19312d83e93438a131d9ab7239193696bc61d70e09e370c98023c45cd2\"}", + "{\"id\":\"5bb1267cbc05f8c9365cc6f73ab79cc800c85fdfbdbe4e3d6d319574937244f6\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100240,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"cutoff 240 b\",\"sig\":\"55d93ad2d0702eb9fdb8e019c5802b75ecef439951ab77c76b18bac5d09a2d465fc6e4618dcc43bb7bfb62690e9cae0d60c5dba99c8f3ab24f3231b4e45c8bdf\"}" + ], + "target_event_json": "{\"id\":\"8cb1b211dc3b8bfbd7f457ffe8f4b067f0b065c2658126ee65a82f43ffb67c5a\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable initial\",\"sig\":\"a60cffce8efd7b1a1372d6d0b175afc49bcd1e7a88b94f3e57ef212a4b7f9dbbb91c058c2af11209a20b2cd30d6ce74886fcf770c726dc8c3e20d0081e0ec1fa\"}" + }, + "expected": { + "address_reference": { + "coordinate": "30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100240, + "request_id": "2ca4618370342696c7daedca89741bdea10f0b496bf24907c367cd549c36697c" + }, + "event_reference": null, + "outcome": "suppressed", + "reason": "deletion_address_reference" + } + }, + { + "id": "nip09_suppress_multiple_event_references_use_lowest_request_id", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"fade0f4ddcd9f2f8b9c6508055ceb58c687bd8b99cdaf5f4b63cc13b16f75278\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"event reference a\",\"sig\":\"36ab6c25bcc0fa22da55a566121cf43210f90a8f0f9243736fd24ff57b4799ac09cf017ef3cd132464b728bb5ff87603535fbb1b566413ed5f34a60c1838a809\"}", + "{\"id\":\"2284746bed9c30d7d7829553a42fa5e53200bf8bd6160a99076ee97e9b3aa75b\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100211,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"event reference b\",\"sig\":\"920891d1e65e9b476e0a0810cde3583bd92586c3e07feb650727ade2aff5487c08f0c37fc669bfd3137082991fc1ac0e467987c3b8468a11107b5b4fcd59a3a5\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": { + "request_id": "2284746bed9c30d7d7829553a42fa5e53200bf8bd6160a99076ee97e9b3aa75b" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + }, + { + "id": "nip09_suppress_repeated_request_is_idempotent", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"fade0f4ddcd9f2f8b9c6508055ceb58c687bd8b99cdaf5f4b63cc13b16f75278\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"event reference a\",\"sig\":\"36ab6c25bcc0fa22da55a566121cf43210f90a8f0f9243736fd24ff57b4799ac09cf017ef3cd132464b728bb5ff87603535fbb1b566413ed5f34a60c1838a809\"}", + "{\"id\":\"fade0f4ddcd9f2f8b9c6508055ceb58c687bd8b99cdaf5f4b63cc13b16f75278\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100210,\"kind\":5,\"tags\":[[\"e\",\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\"]],\"content\":\"event reference a\",\"sig\":\"36ab6c25bcc0fa22da55a566121cf43210f90a8f0f9243736fd24ff57b4799ac09cf017ef3cd132464b728bb5ff87603535fbb1b566413ed5f34a60c1838a809\"}" + ], + "target_event_json": "{\"id\":\"18defc3fb2ba6d3f7d0edf19cf942f888c7403b2a3e36bf21a7efa88ddf4b235\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":1,\"tags\":[],\"content\":\"regular target\",\"sig\":\"0f8eaacba0e40a2cca3ce1ae0d9aba42266c2ccc4d7465f5d056d534ea7abfba4be8de36e2a3262cbda6a207554e8fd50cae2605df98586075e892db1ea7b724\"}" + }, + "expected": { + "address_reference": null, + "event_reference": { + "request_id": "fade0f4ddcd9f2f8b9c6508055ceb58c687bd8b99cdaf5f4b63cc13b16f75278" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + }, + { + "id": "nip09_suppress_unauthorized_event_plus_stale_authorized_address", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"23f5fa17f6c32fbac2be7d3bbac04f772c8736610c508bb058d5176f07fb83cd\",\"pubkey\":\"e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af\",\"created_at\":1800100270,\"kind\":5,\"tags\":[[\"e\",\"8cb1b211dc3b8bfbd7f457ffe8f4b067f0b065c2658126ee65a82f43ffb67c5a\"]],\"content\":\"unauthorized exact addressable event\",\"sig\":\"554ecdb5a70ae3a5a57689a531635876d71e66a9f39deaff4384f1f3a983824c11566c8e007487ef664abdde03f248b18404254f958c5cf20ae2d9cb0c78e7b6\"}", + "{\"id\":\"0e2e9cb53f3c98e254fdbba41f6f87f470dceed991d2fee7c06db5957690b182\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100199,\"kind\":5,\"tags\":[[\"a\",\"30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:\"]],\"content\":\"stale authorized address\",\"sig\":\"2d3b1a3f296eb2558af31611426af71f705a503f6671f9528d6752214fbbbb2b7b47837b9acc7e3f69f1bd068058cad735ea48fbd6ff1f2a1f793eefad165907\"}" + ], + "target_event_json": "{\"id\":\"8cb1b211dc3b8bfbd7f457ffe8f4b067f0b065c2658126ee65a82f43ffb67c5a\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100200,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable initial\",\"sig\":\"a60cffce8efd7b1a1372d6d0b175afc49bcd1e7a88b94f3e57ef212a4b7f9dbbb91c058c2af11209a20b2cd30d6ce74886fcf770c726dc8c3e20d0081e0ec1fa\"}" + }, + "expected": { + "address_reference": { + "coordinate": "30000:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:", + "inclusive_cutoff": 1800100199, + "request_id": "0e2e9cb53f3c98e254fdbba41f6f87f470dceed991d2fee7c06db5957690b182" + }, + "event_reference": null, + "outcome": "visible", + "reason": "deletion_address_cutoff_precedes_target" + } + }, + { + "id": "nip09_suppress_exact_event_deletes_later_replacement", + "kind": "social.deletion_request.evaluate_suppression.valid", + "input": { + "request_event_jsons": [ + "{\"id\":\"5d0e1e5fbd9f7f8afc20b7b53b7387fb5bbeb4cfaa9ab715833efc3ed248f83d\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100270,\"kind\":5,\"tags\":[[\"e\",\"d35339c16ecb0baff3149324c96c01983e4263278a1e1d6f58ad41f538bcd3ed\"]],\"content\":\"exact later replacement\",\"sig\":\"c7beed981d08bb5be1b1938baaac92554bfe0133ac1de85c56c0205c4adcd3f52f2895819faa2d8c1733fdad6589c6fba9547135e5b580a230881621f5d083b8\"}" + ], + "target_event_json": "{\"id\":\"d35339c16ecb0baff3149324c96c01983e4263278a1e1d6f58ad41f538bcd3ed\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1800100260,\"kind\":30000,\"tags\":[[\"d\",\"\"]],\"content\":\"addressable later\",\"sig\":\"55e1bffad1fa0553c1b67f27a15f226c3ea18ba2a2716f12a8ae46cdd007d2c3b2a6d36997521de8a70cc2914dceb1f0fe1664176e27c458069f7c04b9383ad4\"}" + }, + "expected": { + "address_reference": null, + "event_reference": { + "request_id": "5d0e1e5fbd9f7f8afc20b7b53b7387fb5bbeb4cfaa9ab715833efc3ed248f83d" + }, + "outcome": "suppressed", + "reason": "deletion_event_id_reference" + } + } + ] +} diff --git a/crates/event_codec/tests/nip09_suppression_conformance.rs b/crates/event_codec/tests/nip09_suppression_conformance.rs @@ -0,0 +1,587 @@ +#![cfg(all(feature = "serde_json", feature = "nostr"))] + +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet}, + fs, + path::Path, +}; + +use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts}; +use radroots_event_codec::{ + deletion::{ + admission::{ + RadrootsAdmittedNip09DeletionRequestEvent, admit_verified_nip09_deletion_request_event, + }, + evaluator::{RadrootsNip09SuppressionDecision, evaluate_nip09_suppression}, + }, + verification::{RadrootsSignatureVerifiedEvent, verify_nip01_event}, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; + +const PACKAGED_VECTORS: &str = include_str!("fixtures/deletion_suppression.v1.json"); +const WORKSPACE_VECTOR_PATH: &str = + "../../contracts/conformance/vectors/deletion/suppression.v1.json"; +const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml"; +const OPERATION: &str = "social.deletion_request.evaluate_suppression.valid"; + +const VECTOR_EXPECTATIONS: [(&str, &str, &str); 32] = [ + ( + "nip09_suppress_no_requests_visible", + "visible", + "deletion_no_authorized_reference", + ), + ( + "nip09_suppress_same_author_event_reference", + "suppressed", + "deletion_event_id_reference", + ), + ( + "nip09_suppress_same_author_nonmatching_reference", + "visible", + "deletion_no_authorized_reference", + ), + ( + "nip09_suppress_wrong_author_exact_event_reference", + "visible", + "deletion_request_author_mismatch", + ), + ( + "nip09_suppress_event_reference_predates_target", + "suppressed", + "deletion_event_id_reference", + ), + ( + "nip09_suppress_deletion_request_immune_event_reference", + "visible", + "deletion_request_immune", + ), + ( + "nip09_suppress_deletion_request_immune_mixed_references", + "visible", + "deletion_request_immune", + ), + ( + "nip09_suppress_address_cutoff_before_target", + "visible", + "deletion_address_cutoff_precedes_target", + ), + ( + "nip09_suppress_address_cutoff_equal_target", + "suppressed", + "deletion_address_reference", + ), + ( + "nip09_suppress_address_cutoff_after_target", + "suppressed", + "deletion_address_reference", + ), + ( + "nip09_suppress_address_wrong_kind", + "visible", + "deletion_no_authorized_reference", + ), + ( + "nip09_suppress_address_wrong_identifier", + "visible", + "deletion_no_authorized_reference", + ), + ( + "nip09_suppress_address_wrong_pubkey", + "visible", + "deletion_no_authorized_reference", + ), + ( + "nip09_suppress_wrong_author_address_reference", + "visible", + "deletion_request_author_mismatch", + ), + ( + "nip09_suppress_replaceable_kind_0", + "suppressed", + "deletion_address_reference", + ), + ( + "nip09_suppress_replaceable_kind_3", + "suppressed", + "deletion_address_reference", + ), + ( + "nip09_suppress_replaceable_kind_10000", + "suppressed", + "deletion_address_reference", + ), + ( + "nip09_suppress_replaceable_kind_19999", + "suppressed", + "deletion_address_reference", + ), + ( + "nip09_suppress_addressable_kind_30000_empty_identifier", + "suppressed", + "deletion_address_reference", + ), + ( + "nip09_suppress_addressable_kind_39999_opaque_identifier", + "suppressed", + "deletion_address_reference", + ), + ( + "nip09_suppress_combined_event_and_address_references", + "suppressed", + "deletion_event_id_and_address_reference", + ), + ( + "nip09_suppress_event_reference_with_stale_address", + "suppressed", + "deletion_event_id_reference", + ), + ( + "nip09_suppress_kind_advisory_diagnostics_ignored", + "suppressed", + "deletion_event_id_reference", + ), + ( + "nip09_suppress_duplicate_raw_targets_deduplicated", + "suppressed", + "deletion_event_id_and_address_reference", + ), + ( + "nip09_suppress_max_address_cutoff_forward_order", + "suppressed", + "deletion_address_reference", + ), + ( + "nip09_suppress_max_address_cutoff_reverse_order", + "suppressed", + "deletion_address_reference", + ), + ( + "nip09_suppress_later_revision_survives_address_cutoff", + "visible", + "deletion_address_cutoff_precedes_target", + ), + ( + "nip09_suppress_equal_max_cutoff_uses_lowest_request_id", + "suppressed", + "deletion_address_reference", + ), + ( + "nip09_suppress_multiple_event_references_use_lowest_request_id", + "suppressed", + "deletion_event_id_reference", + ), + ( + "nip09_suppress_repeated_request_is_idempotent", + "suppressed", + "deletion_event_id_reference", + ), + ( + "nip09_suppress_unauthorized_event_plus_stale_authorized_address", + "visible", + "deletion_address_cutoff_precedes_target", + ), + ( + "nip09_suppress_exact_event_deletes_later_replacement", + "suppressed", + "deletion_event_id_reference", + ), +]; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Suite { + suite: String, + contract_version: String, + vectors: Vec<Vector>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Vector { + id: String, + kind: String, + input: Input, + expected: ExpectedDecision, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Input { + request_event_jsons: Vec<String>, + target_event_json: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +struct ExpectedDecision { + outcome: String, + reason: String, + event_reference: Option<ExpectedEventReference>, + address_reference: Option<ExpectedAddressReference>, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +struct ExpectedEventReference { + request_id: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +struct ExpectedAddressReference { + coordinate: String, + inclusive_cutoff: u64, + request_id: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct RawEvent { + id: String, + pubkey: String, + created_at: u64, + kind: u32, + tags: Vec<Vec<String>>, + content: String, + sig: String, +} + +#[test] +fn suppression_vectors_execute_the_public_evaluator() { + let vectors = conformance_vectors(); + let raw_suite: Value = + serde_json::from_str(&vectors).expect("NIP-09 suppression JSON must parse"); + validate_no_forbidden_corpus_metadata(&raw_suite, "$") + .unwrap_or_else(|error| panic!("{error}")); + assert_exact_nullable_evidence_shape(&raw_suite); + + let suite: Suite = + serde_json::from_str(&vectors).expect("NIP-09 suppression vectors must parse"); + assert_eq!(suite.suite, "nip09_suppression_evaluator"); + assert_eq!(suite.contract_version, "1.0.0"); + assert_vector_inventory(&suite.vectors); + assert_signed_event_inventory(&suite.vectors); + + for vector in &suite.vectors { + execute(vector); + } +} + +fn assert_vector_inventory(vectors: &[Vector]) { + let expected = VECTOR_EXPECTATIONS + .into_iter() + .map(|(id, outcome, reason)| (id, (OPERATION, outcome, reason))) + .collect::<BTreeMap<_, _>>(); + let actual = vectors + .iter() + .map(|vector| { + ( + vector.id.as_str(), + ( + vector.kind.as_str(), + vector.expected.outcome.as_str(), + vector.expected.reason.as_str(), + ), + ) + }) + .collect::<BTreeMap<_, _>>(); + assert_eq!(vectors.len(), VECTOR_EXPECTATIONS.len()); + assert_eq!(actual, expected); + assert!(vectors.iter().all(|vector| vector.id.starts_with("nip09_"))); + + let reasons = vectors + .iter() + .map(|vector| vector.expected.reason.as_str()) + .collect::<BTreeSet<_>>(); + assert_eq!( + reasons, + BTreeSet::from([ + "deletion_address_cutoff_precedes_target", + "deletion_address_reference", + "deletion_event_id_and_address_reference", + "deletion_event_id_reference", + "deletion_no_authorized_reference", + "deletion_request_author_mismatch", + "deletion_request_immune", + ]) + ); +} + +fn assert_signed_event_inventory(vectors: &[Vector]) { + let event_jsons = vectors + .iter() + .flat_map(|vector| { + std::iter::once(vector.input.target_event_json.as_str()) + .chain(vector.input.request_event_jsons.iter().map(String::as_str)) + }) + .collect::<BTreeSet<_>>(); + assert_eq!(event_jsons.len(), 38, "signed event inventory drifted"); + + let mut deletion_request_count = 0usize; + for event_json in event_jsons { + let verified = verified_event(event_json, "signed event inventory"); + if verified.event().kind_u32() == 5 { + admit_verified_nip09_deletion_request_event(verified) + .expect("every fixed kind-5 corpus event must admit"); + deletion_request_count += 1; + } + } + assert_eq!(deletion_request_count, 30); +} + +fn execute(vector: &Vector) { + let target = verified_event(&vector.input.target_event_json, &vector.id); + let requests = vector + .input + .request_event_jsons + .iter() + .map(|event_json| admitted_request(event_json, &vector.id)) + .collect::<Vec<_>>(); + let target_before = target.clone(); + let requests_before = requests.clone(); + + let decision = evaluate_nip09_suppression(&target, &requests); + assert_eq!(actual_decision(&decision), vector.expected, "{}", vector.id); + assert_eq!(target, target_before, "{} target changed", vector.id); + assert_eq!(requests, requests_before, "{} requests changed", vector.id); + + let repeated = evaluate_nip09_suppression(&target, &requests); + assert_eq!( + repeated, decision, + "{} repeat evaluation drifted", + vector.id + ); + let mut reversed = requests.clone(); + reversed.reverse(); + assert_eq!( + evaluate_nip09_suppression(&target, &reversed), + decision, + "{} request order changed the decision", + vector.id + ); +} + +fn actual_decision(decision: &RadrootsNip09SuppressionDecision) -> ExpectedDecision { + ExpectedDecision { + outcome: decision.outcome().code().to_owned(), + reason: decision.reason().code().to_owned(), + event_reference: decision + .event_reference() + .map(|evidence| ExpectedEventReference { + request_id: evidence.request_id().as_str().to_owned(), + }), + address_reference: decision + .address_reference() + .map(|evidence| ExpectedAddressReference { + coordinate: evidence.coordinate().as_str().to_owned(), + inclusive_cutoff: evidence.inclusive_cutoff(), + request_id: evidence.request_id().as_str().to_owned(), + }), + } +} + +fn admitted_request( + event_json: &str, + vector_id: &str, +) -> RadrootsAdmittedNip09DeletionRequestEvent { + let verified = verified_event(event_json, vector_id); + admit_verified_nip09_deletion_request_event(verified) + .unwrap_or_else(|error| panic!("{vector_id} request admission failed: {error}")) +} + +fn verified_event(event_json: &str, vector_id: &str) -> RadrootsSignatureVerifiedEvent { + verify_nip01_event(event_envelope(event_json, vector_id)) + .unwrap_or_else(|error| panic!("{vector_id} signature verification failed: {error}")) +} + +fn event_envelope(event_json: &str, vector_id: &str) -> RadrootsEventEnvelope { + let raw: RawEvent = serde_json::from_str(event_json) + .unwrap_or_else(|error| panic!("{vector_id} signed event JSON is invalid: {error}")); + assert_eq!( + serde_json::to_string(&raw).expect("raw event serialization"), + event_json, + "{vector_id} signed event JSON must be canonical and compact" + ); + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: raw.id, + author: raw.pubkey, + created_at: raw.created_at, + kind: raw.kind, + tags: raw.tags, + content: raw.content, + sig: raw.sig, + }) + .unwrap_or_else(|error| panic!("{vector_id} event envelope failed: {error}")) +} + +fn assert_exact_nullable_evidence_shape(suite: &Value) { + let suite_object = suite.as_object().expect("suite object"); + assert_eq!( + suite_object + .keys() + .map(String::as_str) + .collect::<BTreeSet<_>>(), + BTreeSet::from(["contract_version", "suite", "vectors"]) + ); + for vector in suite["vectors"].as_array().expect("vectors array") { + assert_eq!( + vector + .as_object() + .expect("vector object") + .keys() + .map(String::as_str) + .collect::<BTreeSet<_>>(), + BTreeSet::from(["expected", "id", "input", "kind"]) + ); + assert_eq!( + vector["input"] + .as_object() + .expect("input object") + .keys() + .map(String::as_str) + .collect::<BTreeSet<_>>(), + BTreeSet::from(["request_event_jsons", "target_event_json"]) + ); + assert_eq!( + vector["expected"] + .as_object() + .expect("expected object") + .keys() + .map(String::as_str) + .collect::<BTreeSet<_>>(), + BTreeSet::from(["address_reference", "event_reference", "outcome", "reason",]) + ); + } +} + +fn conformance_vectors() -> Cow<'static, str> { + let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH); + match fs::read_to_string(&workspace_path) { + Ok(canonical) => { + assert_eq!( + canonical, + PACKAGED_VECTORS, + "packaged suppression vectors must match {}", + workspace_path.display() + ); + Cow::Owned(canonical) + } + Err(error) + if error.kind() == std::io::ErrorKind::NotFound + && !Path::new(env!("CARGO_MANIFEST_DIR")) + .join(WORKSPACE_CONTRACT_MARKER_PATH) + .is_file() => + { + Cow::Borrowed(PACKAGED_VECTORS) + } + Err(error) => panic!("failed to read {}: {error}", workspace_path.display()), + } +} + +fn validate_no_forbidden_corpus_metadata(value: &Value, path: &str) -> Result<(), String> { + match value { + Value::Object(object) => { + for (key, child) in object { + if is_forbidden_metadata_key(key) { + return Err(format!( + "frozen NIP-09 suppression corpus contains forbidden key {path}.{key}" + )); + } + validate_no_forbidden_corpus_metadata(child, &format!("{path}.{key}"))?; + } + } + Value::Array(values) => { + for (index, child) in values.iter().enumerate() { + validate_no_forbidden_corpus_metadata(child, &format!("{path}[{index}]"))?; + } + } + Value::String(string) => { + if string.to_ascii_lowercase().contains("nsec1") { + return Err(format!( + "frozen NIP-09 suppression corpus contains nsec material at {path}" + )); + } + if contains_approved_fixture_secret(string) { + return Err(format!( + "frozen NIP-09 suppression corpus contains a fixture secret at {path}" + )); + } + } + _ => {} + } + Ok(()) +} + +fn is_forbidden_metadata_key(key: &str) -> bool { + let normalized = key.to_ascii_lowercase(); + matches!(normalized.as_str(), "base" | "mutation") + || normalized.contains("source") + || normalized.contains("seed") + || normalized.contains("generator") + || normalized.contains("recipe") + || normalized.contains("secret_key") + || normalized.contains("private_key") + || normalized.contains("signing_key") + || normalized.contains("authorization") + || normalized.contains("authorized") + || normalized.contains("store_mutation") + || normalized == "effect" + || normalized.ends_with("_effect") + || normalized == "effects" +} + +fn contains_approved_fixture_secret(value: &str) -> bool { + let normalized = value.to_ascii_lowercase(); + radroots_test_fixtures::approved_fixture_identities() + .iter() + .any(|identity| normalized.contains(identity.secret_key_hex)) +} + +#[test] +fn suppression_corpus_hygiene_rejects_generation_and_effect_authority_metadata() { + for key in [ + "SOURCE", + "SeEd", + "GENERATOR", + "MuTaTiOn", + "AuThOrIzAtIoN", + "store_effect", + ] { + let value = Value::Object([(key.to_owned(), Value::Null)].into_iter().collect()); + let error = validate_no_forbidden_corpus_metadata(&value, "$") + .expect_err("forbidden corpus metadata must fail"); + assert!(error.contains(key), "{error}"); + } + assert!(validate_no_forbidden_corpus_metadata(&json!("NSEC1FORBIDDEN"), "$").is_err()); + for identity in radroots_test_fixtures::approved_fixture_identities() { + assert!( + validate_no_forbidden_corpus_metadata(&json!(identity.secret_key_hex), "$").is_err() + ); + } +} + +#[test] +fn suppression_corpus_shapes_reject_unknown_fields() { + let input = json!({ + "request_event_jsons": [], + "target_event_json": "{}", + "unexpected": null + }); + serde_json::from_value::<Input>(input).expect_err("unknown input fields must fail"); + + let expected = json!({ + "outcome": "visible", + "reason": "deletion_no_authorized_reference", + "event_reference": null, + "address_reference": null, + "unexpected": null + }); + serde_json::from_value::<ExpectedDecision>(expected) + .expect_err("unknown expected fields must fail"); + + let evidence = json!({"request_id": "a", "unexpected": null}); + serde_json::from_value::<ExpectedEventReference>(evidence) + .expect_err("unknown evidence fields must fail"); +} diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -12,6 +12,7 @@ use deletion_authority::{ DELETION_ADMIT_INVALID_IDS, DELETION_ADMIT_VALID_IDS, DELETION_AUTHORED_INVALID_IDS, DELETION_AUTHORED_VALID_IDS, DELETION_CASE_KINDS, DELETION_CONFORMANCE_VECTOR_RELATIVE, DELETION_OPERATION_EXPECTATIONS, DELETION_PROJECT_INVALID_IDS, DELETION_PROJECT_VALID_IDS, + DELETION_SUPPRESSION_CONFORMANCE_VECTOR_RELATIVE, DELETION_SUPPRESSION_VALID_IDS, REQUIRED_DELETION_PUBLIC_TYPES, }; use semver::Version; @@ -46,7 +47,7 @@ const REPLICA_CONTRACT_NAME: &str = "radroots_replica_contract"; const REPLICA_TRANSFER_CONSTANT: &str = "RADROOTS_REPLICA_TRANSFER_VERSION"; const REPLICA_TRANSFER_VERSION: u32 = 2; const VENDORED_WORKSPACE_MEMBER_RELATIVE: &str = "crates/libsqlite3_sys_3_53_3"; -const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 17] = [ +const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 18] = [ ( "contracts/conformance/vectors/blossom/bud11_claims.v1.json", "crates/blossom/tests/fixtures/bud11_claims.v1.json", @@ -76,6 +77,10 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 17] = [ "crates/event_codec/tests/fixtures/deletion_verified_profile.v1.json", ), ( + "contracts/conformance/vectors/deletion/suppression.v1.json", + "crates/event_codec/tests/fixtures/deletion_suppression.v1.json", + ), + ( "contracts/conformance/vectors/events/operational_listing_tags_full.v1.json", "crates/event_codec/tests/fixtures/operational_listing_tags_full.v1.json", ), @@ -1482,6 +1487,7 @@ struct CommentOperationExpectation { struct DeletionOperationExpectation { key: &'static str, id: &'static str, + vector: &'static str, inputs: &'static [&'static str], outputs: &'static [&'static str], error_class: &'static str, @@ -1860,7 +1866,7 @@ const COMMENT_WITNESSES: [EventBoundarySourceWitness; 8] = [ }, ]; -const DELETION_WITNESSES: [EventBoundarySourceWitness; 7] = [ +const DELETION_WITNESSES: [EventBoundarySourceWitness; 8] = [ EventBoundarySourceWitness { relative_path: "crates/event/src/deletion.rs", required_fragments: &["pub struct RadrootsAuthoredNip09DeletionRequest"], @@ -1884,6 +1890,17 @@ const DELETION_WITNESSES: [EventBoundarySourceWitness; 7] = [ ], }, EventBoundarySourceWitness { + relative_path: "crates/event_codec/src/deletion/evaluator.rs", + required_fragments: &[ + "pub enum RadrootsNip09SuppressionOutcome", + "pub enum RadrootsNip09SuppressionReason", + "pub struct RadrootsNip09EventReferenceEvidence", + "pub struct RadrootsNip09AddressReferenceEvidence", + "pub struct RadrootsNip09SuppressionDecision", + "pub fn evaluate_nip09_suppression", + ], + }, + EventBoundarySourceWitness { relative_path: "crates/nostr/src/events/deletion.rs", required_fragments: &[ "pub struct RadrootsNostrNip09DeletionRequestEventBuilder", @@ -2571,11 +2588,12 @@ const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 44] = [ EventBoundaryExpectation { domain: "deletion_request", kind: "5", - radroots_type: "RadrootsAuthoredNip09DeletionRequest / RadrootsInboundNip09DeletionProjection / RadrootsAdmittedNip09DeletionRequestEvent / RadrootsNostrNip09DeletionRequestEventBuilder", + radroots_type: "RadrootsAuthoredNip09DeletionRequest / RadrootsInboundNip09DeletionProjection / RadrootsAdmittedNip09DeletionRequestEvent / RadrootsNip09SuppressionDecision / RadrootsNostrNip09DeletionRequestEventBuilder", rpc_methods: &[ "social.deletion_request.build_authored_draft", "social.deletion_request.project_verified_event", "social.deletion_request.verify_and_admit_event", + "social.deletion_request.evaluate_suppression", ], witnesses: &DELETION_WITNESSES, }, @@ -4116,20 +4134,28 @@ fn validate_all_conformance_vectors( } let canonical_comment_path = workspace_root.join(COMMENT_CONFORMANCE_VECTOR_RELATIVE); let canonical_deletion_path = workspace_root.join(DELETION_CONFORMANCE_VECTOR_RELATIVE); + let canonical_deletion_suppression_path = + workspace_root.join(DELETION_SUPPRESSION_CONFORMANCE_VECTOR_RELATIVE); for path in paths { let vector = validate_conformance_vector_file(&path, contract_version)?; validate_comment_vector_namespace(&path, &canonical_comment_path, &vector)?; - validate_deletion_vector_namespace(&path, &canonical_deletion_path, &vector)?; + validate_deletion_vector_namespace( + &path, + &canonical_deletion_path, + &canonical_deletion_suppression_path, + &vector, + )?; } Ok(()) } fn validate_deletion_vector_namespace( path: &Path, - canonical_path: &Path, + canonical_request_path: &Path, + canonical_suppression_path: &Path, vector: &ConformanceVectorFile, ) -> Result<(), String> { - if path == canonical_path { + if path == canonical_request_path || path == canonical_suppression_path { return Ok(()); } if let Some(entry) = vector @@ -4138,10 +4164,11 @@ fn validate_deletion_vector_namespace( .find(|entry| entry.kind.starts_with("social.deletion_request.")) { return Err(format!( - "deletion conformance case kind {} in {} is outside canonical vector {}", + "deletion conformance case kind {} in {} is outside canonical vectors {} and {}", entry.kind, path.display(), - canonical_path.display() + canonical_request_path.display(), + canonical_suppression_path.display() )); } Ok(()) @@ -5870,16 +5897,21 @@ fn validate_deletion_operation_authority( manifest: &OperationsContractManifest, workspace_root: &Path, ) -> Result<(), String> { - let vector = validate_conformance_vector_file( + let request_vector = validate_conformance_vector_file( &workspace_root.join(DELETION_CONFORMANCE_VECTOR_RELATIVE), &manifest.contract.version, )?; - validate_deletion_operation_inventory(manifest, &vector) + let suppression_vector = validate_conformance_vector_file( + &workspace_root.join(DELETION_SUPPRESSION_CONFORMANCE_VECTOR_RELATIVE), + &manifest.contract.version, + )?; + validate_deletion_operation_inventory(manifest, &request_vector, &suppression_vector) } fn validate_deletion_operation_inventory( manifest: &OperationsContractManifest, - vector: &ConformanceVectorFile, + request_vector: &ConformanceVectorFile, + suppression_vector: &ConformanceVectorFile, ) -> Result<(), String> { let shared_types = collect_non_empty_set( &manifest.shared_types.public, @@ -5893,6 +5925,7 @@ fn validate_deletion_operation_inventory( .map(String::as_str) .filter(|name| { name.contains("Nip09Deletion") + || name.starts_with("RadrootsNip09") || name.starts_with("RadrootsNip01Coordinate") || matches!( *name, @@ -5918,6 +5951,7 @@ fn validate_deletion_operation_inventory( .iter() .filter(|(key, operation)| { operation.conformance.vector == DELETION_CONFORMANCE_VECTOR_RELATIVE + || operation.conformance.vector == DELETION_SUPPRESSION_CONFORMANCE_VECTOR_RELATIVE || key.starts_with("social_deletion_request_") || operation.id.starts_with("social.deletion_request.") }) @@ -5941,6 +5975,15 @@ fn validate_deletion_operation_inventory( let mut owners = BTreeMap::new(); for expected in DELETION_OPERATION_EXPECTATIONS { + let vector = match expected.vector { + DELETION_CONFORMANCE_VECTOR_RELATIVE => request_vector, + DELETION_SUPPRESSION_CONFORMANCE_VECTOR_RELATIVE => suppression_vector, + unexpected => { + return Err(format!( + "deletion operation authority contains unsupported vector {unexpected}" + )); + } + }; let operation = manifest .operations .get(expected.key) @@ -6005,7 +6048,7 @@ fn validate_deletion_operation_inventory( expected.key, "conformance.vector", &operation.conformance.vector, - DELETION_CONFORMANCE_VECTOR_RELATIVE, + expected.vector, )?; validate_operation_case_kinds(operation, vector)?; if !operation @@ -6040,7 +6083,7 @@ fn validate_deletion_operation_inventory( } let mut actual_inventory = BTreeMap::new(); - for entry in &vector.vectors { + for entry in &request_vector.vectors { validate_deletion_vector_shape(entry)?; if actual_inventory .insert(entry.id.as_str(), entry.kind.as_str()) @@ -6106,6 +6149,409 @@ fn validate_deletion_operation_inventory( )); } + validate_deletion_suppression_vector_inventory(suppression_vector, &owners) +} + +fn validate_deletion_suppression_vector_inventory( + vector: &ConformanceVectorFile, + owners: &BTreeMap<&str, &str>, +) -> Result<(), String> { + if vector.suite != "nip09_suppression_evaluator" { + return Err(format!( + "deletion suppression conformance suite drift: expected nip09_suppression_evaluator, got {}", + vector.suite + )); + } + + let expected_kind = "social.deletion_request.evaluate_suppression.valid"; + if owners.get(expected_kind).copied() != Some("social_deletion_request_evaluate_suppression") { + return Err(format!( + "deletion suppression conformance kind {expected_kind} is not owned by the evaluator operation" + )); + } + + let expected_inventory = DELETION_SUPPRESSION_VALID_IDS + .into_iter() + .map(|id| (id, expected_kind)) + .collect::<BTreeMap<_, _>>(); + let mut actual_inventory = BTreeMap::new(); + for entry in &vector.vectors { + validate_deletion_suppression_vector_shape(entry)?; + if actual_inventory + .insert(entry.id.as_str(), entry.kind.as_str()) + .is_some() + { + return Err(format!( + "deletion suppression conformance vector inventory has duplicate id {}", + entry.id + )); + } + if !entry.id.starts_with("nip09_suppress_") { + return Err(format!( + "deletion suppression conformance vector id {} must use the nip09_suppress_ prefix", + entry.id + )); + } + if entry.kind != expected_kind { + return Err(format!( + "deletion suppression conformance vector {} kind drift: expected {expected_kind}, got {}", + entry.id, entry.kind + )); + } + } + if actual_inventory != expected_inventory { + return Err(format!( + "deletion suppression conformance vector inventory drift: expected {:?}, got {:?}", + expected_inventory, actual_inventory + )); + } + + Ok(()) +} + +fn validate_deletion_suppression_vector_shape( + entry: &ConformanceVectorEntry, +) -> Result<(), String> { + validate_deletion_suppression_forbidden_material(&entry.input, &format!("{}.input", entry.id))?; + validate_deletion_suppression_forbidden_material( + &entry.expected, + &format!("{}.expected", entry.id), + )?; + + let input = deletion_object(&entry.input, &format!("{} input", entry.id))?; + validate_deletion_object_keys( + input, + &format!("{} input", entry.id), + &["request_event_jsons", "target_event_json"], + )?; + let target_event_json = input + .get("target_event_json") + .and_then(Value::as_str) + .ok_or_else(|| { + format!( + "deletion suppression vector {} input.target_event_json must be a string", + entry.id + ) + })?; + validate_deletion_suppression_fixed_event( + target_event_json, + &format!("{} target_event_json", entry.id), + None, + )?; + + let request_event_jsons = input + .get("request_event_jsons") + .and_then(Value::as_array) + .ok_or_else(|| { + format!( + "deletion suppression vector {} input.request_event_jsons must be an array", + entry.id + ) + })?; + let mut request_ids = BTreeSet::new(); + for (index, value) in request_event_jsons.iter().enumerate() { + let event_json = value.as_str().ok_or_else(|| { + format!( + "deletion suppression vector {} input.request_event_jsons[{index}] must be a string", + entry.id + ) + })?; + let event = validate_deletion_suppression_fixed_event( + event_json, + &format!("{} request_event_jsons[{index}]", entry.id), + Some(5), + )?; + request_ids.insert(event.id); + } + + let expected = deletion_object(&entry.expected, &format!("{} expected", entry.id))?; + validate_deletion_object_keys( + expected, + &format!("{} expected", entry.id), + &["address_reference", "event_reference", "outcome", "reason"], + )?; + let outcome = expected + .get("outcome") + .and_then(Value::as_str) + .ok_or_else(|| { + format!( + "deletion suppression vector {} expected.outcome must be a string", + entry.id + ) + })?; + if !matches!(outcome, "visible" | "suppressed") { + return Err(format!( + "deletion suppression vector {} expected.outcome is unsupported: {outcome}", + entry.id + )); + } + let reason = expected + .get("reason") + .and_then(Value::as_str) + .ok_or_else(|| { + format!( + "deletion suppression vector {} expected.reason must be a string", + entry.id + ) + })?; + if !matches!( + reason, + "deletion_request_immune" + | "deletion_no_authorized_reference" + | "deletion_request_author_mismatch" + | "deletion_address_cutoff_precedes_target" + | "deletion_event_id_reference" + | "deletion_address_reference" + | "deletion_event_id_and_address_reference" + ) { + return Err(format!( + "deletion suppression vector {} expected.reason is unsupported: {reason}", + entry.id + )); + } + + let event_reference = validate_deletion_suppression_event_reference( + expected + .get("event_reference") + .expect("exact expected keys contain event_reference"), + &entry.id, + &request_ids, + )?; + let address_reference = validate_deletion_suppression_address_reference( + expected + .get("address_reference") + .expect("exact expected keys contain address_reference"), + &entry.id, + &request_ids, + )?; + let decision_shape_matches = match reason { + "deletion_request_immune" + | "deletion_no_authorized_reference" + | "deletion_request_author_mismatch" => { + outcome == "visible" && !event_reference && !address_reference + } + "deletion_address_cutoff_precedes_target" => { + outcome == "visible" && !event_reference && address_reference + } + "deletion_address_reference" => { + outcome == "suppressed" && !event_reference && address_reference + } + "deletion_event_id_and_address_reference" => { + outcome == "suppressed" && event_reference && address_reference + } + "deletion_event_id_reference" => outcome == "suppressed" && event_reference, + _ => unreachable!("supported reason matched above"), + }; + if !decision_shape_matches { + return Err(format!( + "deletion suppression vector {} expected decision shape is inconsistent with reason {reason}", + entry.id + )); + } + + Ok(()) +} + +fn validate_deletion_suppression_fixed_event( + event_json: &str, + label: &str, + expected_kind: Option<u32>, +) -> Result<DeletionConformanceRawEvent, String> { + if contains_nsec_material(event_json) { + return Err(format!( + "deletion suppression vector {label} contains forbidden nsec material" + )); + } + if contains_approved_fixture_secret(event_json) { + return Err(format!( + "deletion suppression vector {label} contains forbidden approved fixture secret material" + )); + } + let raw = serde_json::from_str::<DeletionConformanceRawEvent>(event_json).map_err(|error| { + format!("deletion suppression vector {label} has invalid fixed event shape: {error}") + })?; + if expected_kind.is_some_and(|kind| raw.kind != kind) { + return Err(format!( + "deletion suppression vector {label} must contain a kind-5 deletion request" + )); + } + let canonical = serde_json::to_string(&raw).map_err(|error| { + format!("deletion suppression vector {label} cannot be reserialized: {error}") + })?; + if canonical != event_json { + return Err(format!( + "deletion suppression vector {label} must be compact canonical JSON" + )); + } + Ok(raw) +} + +fn validate_deletion_suppression_event_reference( + value: &Value, + id: &str, + request_ids: &BTreeSet<String>, +) -> Result<bool, String> { + if value.is_null() { + return Ok(false); + } + let reference = deletion_object(value, &format!("{id} expected.event_reference"))?; + validate_deletion_object_keys( + reference, + &format!("{id} expected.event_reference"), + &["request_id"], + )?; + validate_deletion_suppression_request_id( + reference.get("request_id"), + id, + "event_reference.request_id", + request_ids, + )?; + Ok(true) +} + +fn validate_deletion_suppression_address_reference( + value: &Value, + id: &str, + request_ids: &BTreeSet<String>, +) -> Result<bool, String> { + if value.is_null() { + return Ok(false); + } + let reference = deletion_object(value, &format!("{id} expected.address_reference"))?; + validate_deletion_object_keys( + reference, + &format!("{id} expected.address_reference"), + &["coordinate", "inclusive_cutoff", "request_id"], + )?; + let coordinate = reference + .get("coordinate") + .and_then(Value::as_str) + .ok_or_else(|| { + format!( + "deletion suppression vector {id} expected.address_reference.coordinate must be a string" + ) + })?; + validate_deletion_suppression_coordinate(coordinate, id)?; + if !reference + .get("inclusive_cutoff") + .is_some_and(|value| value.as_u64().is_some()) + { + return Err(format!( + "deletion suppression vector {id} expected.address_reference.inclusive_cutoff must be an unsigned integer" + )); + } + validate_deletion_suppression_request_id( + reference.get("request_id"), + id, + "address_reference.request_id", + request_ids, + )?; + Ok(true) +} + +fn validate_deletion_suppression_request_id( + value: Option<&Value>, + id: &str, + field: &str, + request_ids: &BTreeSet<String>, +) -> Result<(), String> { + let request_id = value.and_then(Value::as_str).ok_or_else(|| { + format!("deletion suppression vector {id} expected.{field} must be a string") + })?; + if request_id.len() != 64 + || !request_id + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(format!( + "deletion suppression vector {id} expected.{field} must be lowercase 64-character hex" + )); + } + if !request_ids.contains(request_id) { + return Err(format!( + "deletion suppression vector {id} expected.{field} must identify an input request" + )); + } + Ok(()) +} + +fn validate_deletion_suppression_coordinate(coordinate: &str, id: &str) -> Result<(), String> { + let mut parts = coordinate.splitn(3, ':'); + let kind_text = parts.next().unwrap_or_default(); + let pubkey = parts.next().unwrap_or_default(); + let identifier = parts.next().ok_or_else(|| { + format!( + "deletion suppression vector {id} expected.address_reference.coordinate has invalid format" + ) + })?; + let kind = kind_text.parse::<u32>().map_err(|_| { + format!( + "deletion suppression vector {id} expected.address_reference.coordinate kind is invalid" + ) + })?; + if kind.to_string() != kind_text + || pubkey.len() != 64 + || !pubkey + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + || (!matches!(kind, 0 | 3) + && !(10_000..=19_999).contains(&kind) + && !(30_000..=39_999).contains(&kind)) + || ((matches!(kind, 0 | 3) || (10_000..=19_999).contains(&kind)) && !identifier.is_empty()) + { + return Err(format!( + "deletion suppression vector {id} expected.address_reference.coordinate is not canonical" + )); + } + Ok(()) +} + +fn validate_deletion_suppression_forbidden_material( + value: &Value, + path: &str, +) -> Result<(), String> { + match value { + Value::Object(object) => { + for (key, child) in object { + let normalized = key.to_ascii_lowercase(); + if matches!(normalized.as_str(), "base" | "mutation") + || normalized.contains("seed") + || normalized.contains("generator") + || normalized.contains("recipe") + || normalized.contains("secret_key") + || normalized.contains("private_key") + || normalized.contains("signing_key") + || normalized.contains("boundary") + { + return Err(format!( + "deletion suppression vector contains forbidden metadata key {path}.{key}" + )); + } + validate_deletion_suppression_forbidden_material(child, &format!("{path}.{key}"))?; + } + } + Value::Array(values) => { + for (index, child) in values.iter().enumerate() { + validate_deletion_suppression_forbidden_material( + child, + &format!("{path}[{index}]"), + )?; + } + } + Value::String(string) => { + if contains_nsec_material(string) { + return Err(format!( + "deletion suppression vector contains forbidden nsec material at {path}" + )); + } + if contains_approved_fixture_secret(string) { + return Err(format!( + "deletion suppression vector contains forbidden approved fixture secret material at {path}" + )); + } + } + _ => {} + } Ok(()) } @@ -7985,15 +8431,23 @@ mod tests { (manifest, vector) } - fn current_deletion_authority() -> (OperationsContractManifest, ConformanceVectorFile) { + fn current_deletion_authority() -> ( + OperationsContractManifest, + ConformanceVectorFile, + ConformanceVectorFile, + ) { let root = workspace_root(); let manifest = parse_toml::<OperationsContractManifest>(&root.join("contracts/operations.toml")) .expect("current operations manifest"); - let vector = + let request_vector = parse_json::<ConformanceVectorFile>(&root.join(DELETION_CONFORMANCE_VECTOR_RELATIVE)) .expect("current deletion conformance vector"); - (manifest, vector) + let suppression_vector = parse_json::<ConformanceVectorFile>( + &root.join(DELETION_SUPPRESSION_CONFORMANCE_VECTOR_RELATIVE), + ) + .expect("current deletion suppression conformance vector"); + (manifest, request_vector, suppression_vector) } fn current_food_availability_authority() -> (OperationsContractManifest, ConformanceVectorFile) @@ -9030,34 +9484,36 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"] #[test] fn deletion_operation_authority_rejects_contract_drift() { - let (manifest, vector) = current_deletion_authority(); - validate_deletion_operation_inventory(&manifest, &vector) + let (manifest, request_vector, suppression_vector) = current_deletion_authority(); + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) .expect("current deletion operation authority"); - let (mut manifest, vector) = current_deletion_authority(); + let (mut manifest, request_vector, suppression_vector) = current_deletion_authority(); manifest .operations .remove("social_deletion_request_project_verified_event"); - let error = validate_deletion_operation_inventory(&manifest, &vector) - .expect_err("missing deletion operation must fail"); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("missing deletion operation must fail"); assert!( error.contains("deletion operation authority drift"), "{error}" ); - let (mut manifest, vector) = current_deletion_authority(); + let (mut manifest, request_vector, suppression_vector) = current_deletion_authority(); manifest .shared_types .public .push("RadrootsNip09DeletionUnauthorizedEffect".to_string()); - let error = validate_deletion_operation_inventory(&manifest, &vector) - .expect_err("unexpected deletion public type must fail"); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("unexpected deletion public type must fail"); assert!( error.contains("deletion operation public-type authority drift"), "{error}" ); - let (mut manifest, vector) = current_deletion_authority(); + let (mut manifest, request_vector, suppression_vector) = current_deletion_authority(); manifest .operations .get_mut("social_deletion_request_verify_and_admit_event") @@ -9065,53 +9521,83 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"] .conformance .case_kinds .pop(); - let error = validate_deletion_operation_inventory(&manifest, &vector) - .expect_err("missing deletion case kind must fail"); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("missing deletion case kind must fail"); assert!(error.contains("conformance.case_kinds drift"), "{error}"); + + let (mut manifest, request_vector, suppression_vector) = current_deletion_authority(); + manifest + .operations + .get_mut("social_deletion_request_evaluate_suppression") + .expect("suppression operation") + .conformance + .vector = DELETION_CONFORMANCE_VECTOR_RELATIVE.to_string(); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("suppression vector ownership drift must fail"); + assert!(error.contains("conformance.vector drift"), "{error}"); } #[test] fn deletion_operation_authority_rejects_vector_inventory_drift() { - let (manifest, mut vector) = current_deletion_authority(); - vector + let (manifest, mut request_vector, suppression_vector) = current_deletion_authority(); + request_vector .vectors .iter_mut() .find(|entry| entry.id == "nip09_authored_event_target_min_kind_empty_content") .expect("authored deletion vector") .id = "nip09_authored_event_target_min_kind_replacement".to_string(); - let error = validate_deletion_operation_inventory(&manifest, &vector) - .expect_err("same-count deletion vector ID replacement must fail"); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("same-count deletion vector ID replacement must fail"); assert!( error.contains("deletion conformance vector inventory drift"), "{error}" ); - let (manifest, mut vector) = current_deletion_authority(); - let source = vector + let (manifest, mut request_vector, suppression_vector) = current_deletion_authority(); + let source = request_vector .vectors .iter() .find(|entry| entry.id == "nip09_project_signed_event_target_without_k") .expect("valid signed deletion vector"); let input = source.input.clone(); let expected = source.expected.clone(); - vector.vectors.push(ConformanceVectorEntry { + request_vector.vectors.push(ConformanceVectorEntry { id: "nip09_unclaimed_case".to_string(), kind: "social.deletion_request.unclaimed.valid".to_string(), input, expected, }); - let error = validate_deletion_operation_inventory(&manifest, &vector) - .expect_err("unclaimed deletion vector kind must fail"); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("unclaimed deletion vector kind must fail"); assert!( error.contains("is not claimed by exactly one operation"), "{error}" ); + + let (manifest, request_vector, mut suppression_vector) = current_deletion_authority(); + suppression_vector + .vectors + .iter_mut() + .find(|entry| entry.id == "nip09_suppress_no_requests_visible") + .expect("visible suppression vector") + .id = "nip09_suppress_replacement_visible".to_string(); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("same-count suppression vector ID replacement must fail"); + assert!( + error.contains("deletion suppression conformance vector inventory drift"), + "{error}" + ); } #[test] fn deletion_operation_authority_rejects_generation_and_effect_metadata() { - let (manifest, mut vector) = current_deletion_authority(); - vector + let (manifest, mut request_vector, suppression_vector) = current_deletion_authority(); + request_vector .vectors .iter_mut() .find(|entry| entry.id == "nip09_authored_event_target_min_kind_empty_content") @@ -9120,12 +9606,13 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"] .as_object_mut() .expect("authored input") .insert("SeEd".to_string(), Value::from(7_u64)); - let error = validate_deletion_operation_inventory(&manifest, &vector) - .expect_err("generation seed metadata must fail"); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("generation seed metadata must fail"); assert!(error.contains("forbidden metadata key"), "{error}"); - let (manifest, mut vector) = current_deletion_authority(); - vector + let (manifest, mut request_vector, suppression_vector) = current_deletion_authority(); + request_vector .vectors .iter_mut() .find(|entry| entry.id == "nip09_project_signed_event_target_without_k") @@ -9134,12 +9621,13 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"] .as_object_mut() .expect("signed input") .insert("trace".to_string(), Value::Bool(true)); - let error = validate_deletion_operation_inventory(&manifest, &vector) - .expect_err("extra signed input key must fail"); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("extra signed input key must fail"); assert!(error.contains("input keys drift"), "{error}"); - let (manifest, mut vector) = current_deletion_authority(); - vector + let (manifest, mut request_vector, suppression_vector) = current_deletion_authority(); + request_vector .vectors .iter_mut() .find(|entry| entry.id == "nip09_project_signed_event_target_without_k") @@ -9148,12 +9636,13 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"] .as_object_mut() .expect("projection expected") .insert("AuThOrIzAtIoN".to_string(), Value::Bool(true)); - let error = validate_deletion_operation_inventory(&manifest, &vector) - .expect_err("effect-authority output must fail"); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("effect-authority output must fail"); assert!(error.contains("forbidden metadata key"), "{error}"); - let (manifest, mut vector) = current_deletion_authority(); - vector + let (manifest, mut request_vector, suppression_vector) = current_deletion_authority(); + request_vector .vectors .iter_mut() .find(|entry| entry.id == "nip09_admit_signed_event_target") @@ -9165,12 +9654,13 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"] "event_json".to_string(), Value::String(r#"{"content":"NSEC1FORBIDDEN"}"#.to_string()), ); - let error = validate_deletion_operation_inventory(&manifest, &vector) - .expect_err("nsec material must fail"); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("nsec material must fail"); assert!(error.contains("forbidden nsec material"), "{error}"); - let (manifest, mut vector) = current_deletion_authority(); - vector + let (manifest, mut request_vector, suppression_vector) = current_deletion_authority(); + request_vector .vectors .iter_mut() .find(|entry| entry.id == "nip09_admit_signed_event_target") @@ -9185,8 +9675,9 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"] .to_string(), ), ); - let error = validate_deletion_operation_inventory(&manifest, &vector) - .expect_err("approved fixture secret material must fail"); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("approved fixture secret material must fail"); assert!( error.contains("forbidden approved fixture secret material"), "{error}" @@ -9194,8 +9685,56 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"] } #[test] + fn deletion_suppression_authority_rejects_shape_and_material_drift() { + let (manifest, request_vector, mut suppression_vector) = current_deletion_authority(); + suppression_vector.suite = "nip09_suppression_shadow".to_string(); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("suppression suite drift must fail"); + assert!( + error.contains("deletion suppression conformance suite drift"), + "{error}" + ); + + let (manifest, request_vector, mut suppression_vector) = current_deletion_authority(); + suppression_vector + .vectors + .iter_mut() + .find(|entry| entry.id == "nip09_suppress_no_requests_visible") + .expect("visible suppression vector") + .input + .as_object_mut() + .expect("suppression input") + .insert("seed".to_string(), Value::from(7_u64)); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("suppression generation metadata must fail"); + assert!(error.contains("forbidden metadata key"), "{error}"); + + let (manifest, request_vector, mut suppression_vector) = current_deletion_authority(); + suppression_vector + .vectors + .iter_mut() + .find(|entry| entry.id == "nip09_suppress_same_author_event_reference") + .expect("event-reference suppression vector") + .expected + .as_object_mut() + .expect("suppression expected") + .get_mut("event_reference") + .expect("event-reference evidence") + .as_object_mut() + .expect("event-reference object") + .insert("request_id".to_string(), Value::String("0".repeat(64))); + let error = + validate_deletion_operation_inventory(&manifest, &request_vector, &suppression_vector) + .expect_err("evidence not bound to an input request must fail"); + assert!(error.contains("must identify an input request"), "{error}"); + } + + #[test] fn deletion_vector_namespace_rejects_alternate_owners() { - let canonical = PathBuf::from(DELETION_CONFORMANCE_VECTOR_RELATIVE); + let canonical_request = PathBuf::from(DELETION_CONFORMANCE_VECTOR_RELATIVE); + let canonical_suppression = PathBuf::from(DELETION_SUPPRESSION_CONFORMANCE_VECTOR_RELATIVE); let alternate = PathBuf::from("contracts/conformance/vectors/social/mvp.v1.json"); let vector = ConformanceVectorFile { suite: "alternate".to_string(), @@ -9208,11 +9747,28 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"] }], }; - let error = validate_deletion_vector_namespace(&alternate, &canonical, &vector) - .expect_err("alternate deletion vector namespace must fail"); - assert!(error.contains("outside canonical vector"), "{error}"); - validate_deletion_vector_namespace(&canonical, &canonical, &vector) - .expect("canonical deletion vector owns the namespace"); + let error = validate_deletion_vector_namespace( + &alternate, + &canonical_request, + &canonical_suppression, + &vector, + ) + .expect_err("alternate deletion vector namespace must fail"); + assert!(error.contains("outside canonical vectors"), "{error}"); + validate_deletion_vector_namespace( + &canonical_request, + &canonical_request, + &canonical_suppression, + &vector, + ) + .expect("canonical deletion vector owns the namespace"); + validate_deletion_vector_namespace( + &canonical_suppression, + &canonical_request, + &canonical_suppression, + &vector, + ) + .expect("canonical deletion suppression vector owns the namespace"); } #[test] diff --git a/tools/xtask/src/contract/deletion_authority.rs b/tools/xtask/src/contract/deletion_authority.rs @@ -2,8 +2,10 @@ use super::DeletionOperationExpectation; pub(super) const DELETION_CONFORMANCE_VECTOR_RELATIVE: &str = "contracts/conformance/vectors/deletion/verified_profile.v1.json"; +pub(super) const DELETION_SUPPRESSION_CONFORMANCE_VECTOR_RELATIVE: &str = + "contracts/conformance/vectors/deletion/suppression.v1.json"; -pub(super) const REQUIRED_DELETION_PUBLIC_TYPES: [&str; 18] = [ +pub(super) const REQUIRED_DELETION_PUBLIC_TYPES: [&str; 23] = [ "RadrootsNip01EventWireParts", "RadrootsEventEnvelope", "RadrootsSignatureVerifiedEvent", @@ -22,12 +24,18 @@ pub(super) const REQUIRED_DELETION_PUBLIC_TYPES: [&str; 18] = [ "RadrootsNip09DeletionProjectionError", "RadrootsAdmittedNip09DeletionRequestEvent", "RadrootsNip09DeletionAdmissionError", + "RadrootsNip09SuppressionOutcome", + "RadrootsNip09SuppressionReason", + "RadrootsNip09EventReferenceEvidence", + "RadrootsNip09AddressReferenceEvidence", + "RadrootsNip09SuppressionDecision", ]; -pub(super) const DELETION_OPERATION_EXPECTATIONS: [DeletionOperationExpectation; 3] = [ +pub(super) const DELETION_OPERATION_EXPECTATIONS: [DeletionOperationExpectation; 4] = [ DeletionOperationExpectation { key: "social_deletion_request_build_authored_draft", id: "social.deletion_request.build_authored_draft", + vector: DELETION_CONFORMANCE_VECTOR_RELATIVE, inputs: &["RadrootsAuthoredNip09DeletionRequest"], outputs: &["RadrootsNip01EventWireParts"], error_class: "encode_error", @@ -52,6 +60,7 @@ pub(super) const DELETION_OPERATION_EXPECTATIONS: [DeletionOperationExpectation; DeletionOperationExpectation { key: "social_deletion_request_project_verified_event", id: "social.deletion_request.project_verified_event", + vector: DELETION_CONFORMANCE_VECTOR_RELATIVE, inputs: &["RadrootsSignatureVerifiedEvent"], outputs: &["RadrootsInboundNip09DeletionProjection"], error_class: "parse_error", @@ -77,6 +86,7 @@ pub(super) const DELETION_OPERATION_EXPECTATIONS: [DeletionOperationExpectation; DeletionOperationExpectation { key: "social_deletion_request_verify_and_admit_event", id: "social.deletion_request.verify_and_admit_event", + vector: DELETION_CONFORMANCE_VECTOR_RELATIVE, inputs: &["RadrootsEventEnvelope"], outputs: &["RadrootsAdmittedNip09DeletionRequestEvent"], error_class: "admission_error", @@ -99,15 +109,39 @@ pub(super) const DELETION_OPERATION_EXPECTATIONS: [DeletionOperationExpectation; "social.deletion_request.verify_and_admit_event.invalid", ], }, + DeletionOperationExpectation { + key: "social_deletion_request_evaluate_suppression", + id: "social.deletion_request.evaluate_suppression", + vector: DELETION_SUPPRESSION_CONFORMANCE_VECTOR_RELATIVE, + inputs: &[ + "RadrootsSignatureVerifiedEvent", + "RadrootsAdmittedNip09DeletionRequestEvent[]", + ], + outputs: &["RadrootsNip09SuppressionDecision"], + error_class: "validation_error", + signing: "none", + rust_modules: &["crates/event_codec/src/deletion/evaluator.rs"], + rust_types: &[ + "radroots_event_codec::deletion::admission::RadrootsAdmittedNip09DeletionRequestEvent", + "radroots_event_codec::deletion::evaluator::RadrootsNip09AddressReferenceEvidence", + "radroots_event_codec::deletion::evaluator::RadrootsNip09EventReferenceEvidence", + "radroots_event_codec::deletion::evaluator::RadrootsNip09SuppressionDecision", + "radroots_event_codec::deletion::evaluator::RadrootsNip09SuppressionOutcome", + "radroots_event_codec::deletion::evaluator::RadrootsNip09SuppressionReason", + "radroots_event_codec::verification::RadrootsSignatureVerifiedEvent", + ], + case_kinds: &["social.deletion_request.evaluate_suppression.valid"], + }, ]; -pub(super) const DELETION_CASE_KINDS: [&str; 6] = [ +pub(super) const DELETION_CASE_KINDS: [&str; 7] = [ "social.deletion_request.build_authored_draft.valid", "social.deletion_request.build_authored_draft.invalid", "social.deletion_request.project_verified_event.valid", "social.deletion_request.project_verified_event.invalid", "social.deletion_request.verify_and_admit_event.valid", "social.deletion_request.verify_and_admit_event.invalid", + "social.deletion_request.evaluate_suppression.valid", ]; pub(super) const DELETION_AUTHORED_VALID_IDS: [&str; 14] = [ @@ -207,3 +241,38 @@ pub(super) const DELETION_ADMIT_INVALID_IDS: [&str; 5] = [ "nip09_admit_invalid_target", "nip09_admit_target_missing", ]; + +pub(super) const DELETION_SUPPRESSION_VALID_IDS: [&str; 32] = [ + "nip09_suppress_no_requests_visible", + "nip09_suppress_same_author_event_reference", + "nip09_suppress_same_author_nonmatching_reference", + "nip09_suppress_wrong_author_exact_event_reference", + "nip09_suppress_event_reference_predates_target", + "nip09_suppress_deletion_request_immune_event_reference", + "nip09_suppress_deletion_request_immune_mixed_references", + "nip09_suppress_address_cutoff_before_target", + "nip09_suppress_address_cutoff_equal_target", + "nip09_suppress_address_cutoff_after_target", + "nip09_suppress_address_wrong_kind", + "nip09_suppress_address_wrong_identifier", + "nip09_suppress_address_wrong_pubkey", + "nip09_suppress_wrong_author_address_reference", + "nip09_suppress_replaceable_kind_0", + "nip09_suppress_replaceable_kind_3", + "nip09_suppress_replaceable_kind_10000", + "nip09_suppress_replaceable_kind_19999", + "nip09_suppress_addressable_kind_30000_empty_identifier", + "nip09_suppress_addressable_kind_39999_opaque_identifier", + "nip09_suppress_combined_event_and_address_references", + "nip09_suppress_event_reference_with_stale_address", + "nip09_suppress_kind_advisory_diagnostics_ignored", + "nip09_suppress_duplicate_raw_targets_deduplicated", + "nip09_suppress_max_address_cutoff_forward_order", + "nip09_suppress_max_address_cutoff_reverse_order", + "nip09_suppress_later_revision_survives_address_cutoff", + "nip09_suppress_equal_max_cutoff_uses_lowest_request_id", + "nip09_suppress_multiple_event_references_use_lowest_request_id", + "nip09_suppress_repeated_request_is_idempotent", + "nip09_suppress_unauthorized_event_plus_stale_authorized_address", + "nip09_suppress_exact_event_deletes_later_replacement", +];