lib

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

commit fa65b09bd7c882f3431a54874c8f16cb1ca695e1
parent 91d3ebf3075e07d796b897500b85adc321f167ff
Author: triesap <tyson@radroots.org>
Date:   Sun,  5 Jul 2026 06:09:29 +0000

events-codec: add knowledge fixture manifest

- add deterministic knowledge manifest JSON and SHA-256 artifacts
- add golden and adversarial knowledge fixtures with codec coverage
- validate manifest drift through xtask contract checks
- extend contract metadata for knowledge manifest and decode operations

Diffstat:
MCargo.lock | 7+++++++
Acontracts/conformance/vectors/knowledge/manifest_and_decode.v1.json | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontracts/conformance/vectors/knowledge/regenproto_compatibility.v1.json | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontracts/knowledge/knowledge_event_contract_manifest.v1.json | 636+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontracts/knowledge/knowledge_event_contract_manifest.v1.sha256 | 1+
Mcontracts/operations.toml | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/events/src/contract.rs | 32++++++++++++++++++++++++++++++--
Mcrates/events/src/lib.rs | 2++
Mcrates/events_codec/Cargo.toml | 4++++
Mcrates/events_codec/src/farm_crdt/decode.rs | 5+----
Mcrates/events_codec/src/farm_workspace/decode.rs | 5+----
Mcrates/events_codec/src/lib.rs | 7+++++++
Acrates/events_codec/src/manifest.rs | 377+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events_codec/src/order/decode.rs | 2+-
Mcrates/events_codec/src/order/encode.rs | 3---
Mcrates/events_codec/src/verification.rs | 5+----
Acrates/events_codec/tests/knowledge_fixtures.rs | 361+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/events_codec/tests/knowledge_manifest.rs | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/test_fixtures/Cargo.toml | 8++++++++
Acrates/test_fixtures/src/knowledge.rs | 462+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/test_fixtures/src/lib.rs | 2++
Mtools/xtask/Cargo.toml | 3+++
Mtools/xtask/src/contract.rs | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/main.rs | 10++++++++++
24 files changed, 2327 insertions(+), 20 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4311,11 +4311,14 @@ dependencies = [ name = "radroots_events_codec" version = "0.1.0-alpha.2" dependencies = [ + "hex", "nostr", "radroots_core", "radroots_events", + "radroots_test_fixtures", "serde", "serde_json", + "sha2", ] [[package]] @@ -4819,6 +4822,9 @@ dependencies = [ [[package]] name = "radroots_test_fixtures" version = "0.1.0-alpha.2" +dependencies = [ + "radroots_events", +] [[package]] name = "radroots_trade" @@ -8591,6 +8597,7 @@ name = "xtask" version = "0.1.0-alpha.2" dependencies = [ "hex", + "radroots_events_codec", "serde", "serde_json", "sha2", diff --git a/contracts/conformance/vectors/knowledge/manifest_and_decode.v1.json b/contracts/conformance/vectors/knowledge/manifest_and_decode.v1.json @@ -0,0 +1,58 @@ +{ + "suite": "knowledge_manifest_and_decode", + "contract_version": "0.1.0", + "vectors": [ + { + "id": "knowledge_manifest_fields_valid_001", + "kind": "knowledge.contract_manifest_json.valid", + "input": { + "registry": "radroots_event_contract_registry_v1" + }, + "expected": { + "schema_version": 1, + "required_fields": [ + "schema_version", + "registry_version", + "radroots_events_version", + "radroots_events_codec_version", + "contract_id", + "kind", + "class", + "standard", + "stability", + "privacy", + "content_schema", + "discriminators", + "tag_contracts", + "codec_support", + "sdk_builder_support", + "deprecated", + "replaced_by", + "introduced_at" + ] + } + }, + { + "id": "knowledge_verify_decode_claim_valid_002", + "kind": "knowledge.verify_and_decode_event.valid", + "input": { + "fixture": "knowledge_claim_valid" + }, + "expected": { + "contract_id": "radroots.knowledge.claim.v1", + "decoded_variant": "KnowledgeClaim" + } + }, + { + "id": "knowledge_missing_contract_invalid_003", + "kind": "knowledge.verify_and_decode_event.invalid", + "input": { + "fixture": "missing_contract_id" + }, + "expected": { + "error_class": "decode_error", + "stage": "contract_validation" + } + } + ] +} diff --git a/contracts/conformance/vectors/knowledge/regenproto_compatibility.v1.json b/contracts/conformance/vectors/knowledge/regenproto_compatibility.v1.json @@ -0,0 +1,61 @@ +{ + "suite": "knowledge_downstream_compatibility", + "contract_version": "0.1.0", + "vectors": [ + { + "id": "knowledge_source_downstream_valid_001", + "kind": "knowledge.downstream_compatibility.valid", + "input": { + "fixture": "knowledge_source_valid" + }, + "expected": { + "contract_id": "radroots.knowledge.source.v1", + "operation": "build_validate_decode_expose" + } + }, + { + "id": "knowledge_claim_downstream_valid_002", + "kind": "knowledge.downstream_compatibility.valid", + "input": { + "fixture": "knowledge_claim_valid" + }, + "expected": { + "contract_id": "radroots.knowledge.claim.v1", + "operation": "build_validate_decode_expose" + } + }, + { + "id": "knowledge_review_downstream_valid_003", + "kind": "knowledge.downstream_compatibility.valid", + "input": { + "fixture": "knowledge_review_valid" + }, + "expected": { + "contract_id": "radroots.knowledge.review.v1", + "operation": "build_validate_decode_expose" + } + }, + { + "id": "knowledge_field_report_downstream_valid_004", + "kind": "knowledge.downstream_compatibility.valid", + "input": { + "fixture": "knowledge_field_report_valid" + }, + "expected": { + "contract_id": "radroots.knowledge.field_report.v1", + "operation": "build_validate_decode_expose" + } + }, + { + "id": "wiki_article_downstream_valid_005", + "kind": "knowledge.downstream_compatibility.valid", + "input": { + "fixture": "wiki_article_valid" + }, + "expected": { + "contract_id": "radroots.wiki.article.v1", + "operation": "build_validate_decode_expose" + } + } + ] +} diff --git a/contracts/knowledge/knowledge_event_contract_manifest.v1.json b/contracts/knowledge/knowledge_event_contract_manifest.v1.json @@ -0,0 +1,636 @@ +{ + "schema_version": 1, + "registry_version": 1, + "radroots_events_version": "0.1.0-alpha.2", + "radroots_events_codec_version": "0.1.0-alpha.2", + "contract_count": 11, + "contracts": [ + { + "contract_id": "radroots.knowledge.change_proposal.v1", + "kind": 3464, + "class": "regular", + "standard": "radroots", + "stability": "stable", + "privacy": "public", + "author_role": "any", + "content_schema": "json_object", + "payload_type": "RadrootsKnowledgeChangeProposal", + "discriminators": [ + { + "type": "tag_equals", + "name": "contract", + "value": "radroots.knowledge.change_proposal.v1" + } + ], + "tag_contracts": [ + { + "name": "contract", + "cardinality": "required_one", + "semantic": "contract", + "value_type": "contract_id", + "relay_indexed": false + }, + { + "name": "evidence", + "cardinality": "optional_many", + "semantic": "evidence", + "value_type": "event_pointer", + "relay_indexed": false + } + ], + "reducers": [ + "knowledge_projection" + ], + "codec_support": { + "encode": true, + "decode": true, + "contract_validation": true, + "verified_decode": true, + "verified_decode_requires_nostr": true + }, + "sdk_builder_support": false, + "deprecated": false, + "replaced_by": null, + "introduced_at": "0.1.0-alpha.2" + }, + { + "contract_id": "radroots.knowledge.claim.v1", + "kind": 3460, + "class": "regular", + "standard": "radroots", + "stability": "stable", + "privacy": "public", + "author_role": "any", + "content_schema": "json_object", + "payload_type": "RadrootsKnowledgeClaim", + "discriminators": [ + { + "type": "tag_equals", + "name": "contract", + "value": "radroots.knowledge.claim.v1" + } + ], + "tag_contracts": [ + { + "name": "contract", + "cardinality": "required_one", + "semantic": "contract", + "value_type": "contract_id", + "relay_indexed": false + }, + { + "name": "t", + "cardinality": "optional_many", + "semantic": "topic", + "value_type": "text", + "relay_indexed": true + }, + { + "name": "source", + "cardinality": "optional_many", + "semantic": "source", + "value_type": "event_pointer", + "relay_indexed": false + }, + { + "name": "citation", + "cardinality": "optional_many", + "semantic": "citation", + "value_type": "sha256", + "relay_indexed": false + } + ], + "reducers": [ + "knowledge_projection" + ], + "codec_support": { + "encode": true, + "decode": true, + "contract_validation": true, + "verified_decode": true, + "verified_decode_requires_nostr": true + }, + "sdk_builder_support": false, + "deprecated": false, + "replaced_by": null, + "introduced_at": "0.1.0-alpha.2" + }, + { + "contract_id": "radroots.knowledge.contribution_attestation.v1", + "kind": 3465, + "class": "regular", + "standard": "radroots", + "stability": "stable", + "privacy": "public", + "author_role": "any", + "content_schema": "json_object", + "payload_type": "RadrootsContributionAttestation", + "discriminators": [ + { + "type": "tag_equals", + "name": "contract", + "value": "radroots.knowledge.contribution_attestation.v1" + } + ], + "tag_contracts": [ + { + "name": "contract", + "cardinality": "required_one", + "semantic": "contract", + "value_type": "contract_id", + "relay_indexed": false + }, + { + "name": "evidence", + "cardinality": "optional_many", + "semantic": "evidence", + "value_type": "event_pointer", + "relay_indexed": false + } + ], + "reducers": [ + "knowledge_projection" + ], + "codec_support": { + "encode": true, + "decode": true, + "contract_validation": true, + "verified_decode": true, + "verified_decode_requires_nostr": true + }, + "sdk_builder_support": false, + "deprecated": false, + "replaced_by": null, + "introduced_at": "0.1.0-alpha.2" + }, + { + "contract_id": "radroots.knowledge.evidence_bounty.v1", + "kind": 30451, + "class": "addressable", + "standard": "radroots", + "stability": "stable", + "privacy": "public", + "author_role": "any", + "content_schema": "json_object", + "payload_type": "RadrootsEvidenceBounty", + "discriminators": [ + { + "type": "tag_equals", + "name": "contract", + "value": "radroots.knowledge.evidence_bounty.v1" + } + ], + "tag_contracts": [ + { + "name": "d", + "cardinality": "required_one", + "semantic": "identifier", + "value_type": "d_tag", + "relay_indexed": true + }, + { + "name": "contract", + "cardinality": "required_one", + "semantic": "contract", + "value_type": "contract_id", + "relay_indexed": false + }, + { + "name": "t", + "cardinality": "optional_many", + "semantic": "topic", + "value_type": "text", + "relay_indexed": true + }, + { + "name": "evidence", + "cardinality": "optional_many", + "semantic": "evidence", + "value_type": "event_pointer", + "relay_indexed": false + } + ], + "reducers": [ + "knowledge_projection" + ], + "codec_support": { + "encode": true, + "decode": true, + "contract_validation": true, + "verified_decode": true, + "verified_decode_requires_nostr": true + }, + "sdk_builder_support": false, + "deprecated": false, + "replaced_by": null, + "introduced_at": "0.1.0-alpha.2" + }, + { + "contract_id": "radroots.knowledge.field_report.v1", + "kind": 3463, + "class": "regular", + "standard": "radroots", + "stability": "stable", + "privacy": "public", + "author_role": "any", + "content_schema": "json_object", + "payload_type": "RadrootsKnowledgeFieldReport", + "discriminators": [ + { + "type": "tag_equals", + "name": "contract", + "value": "radroots.knowledge.field_report.v1" + } + ], + "tag_contracts": [ + { + "name": "contract", + "cardinality": "required_one", + "semantic": "contract", + "value_type": "contract_id", + "relay_indexed": false + }, + { + "name": "t", + "cardinality": "optional_many", + "semantic": "topic", + "value_type": "text", + "relay_indexed": true + }, + { + "name": "g", + "cardinality": "optional_one", + "semantic": "geohash", + "value_type": "geohash", + "relay_indexed": true + }, + { + "name": "evidence", + "cardinality": "optional_many", + "semantic": "evidence", + "value_type": "event_pointer", + "relay_indexed": false + } + ], + "reducers": [ + "knowledge_projection" + ], + "codec_support": { + "encode": true, + "decode": true, + "contract_validation": true, + "verified_decode": true, + "verified_decode_requires_nostr": true + }, + "sdk_builder_support": false, + "deprecated": false, + "replaced_by": null, + "introduced_at": "0.1.0-alpha.2" + }, + { + "contract_id": "radroots.knowledge.relation.v1", + "kind": 3461, + "class": "regular", + "standard": "radroots", + "stability": "stable", + "privacy": "public", + "author_role": "any", + "content_schema": "json_object", + "payload_type": "RadrootsKnowledgeRelation", + "discriminators": [ + { + "type": "tag_equals", + "name": "contract", + "value": "radroots.knowledge.relation.v1" + } + ], + "tag_contracts": [ + { + "name": "contract", + "cardinality": "required_one", + "semantic": "contract", + "value_type": "contract_id", + "relay_indexed": false + }, + { + "name": "t", + "cardinality": "optional_many", + "semantic": "topic", + "value_type": "text", + "relay_indexed": true + }, + { + "name": "source", + "cardinality": "optional_many", + "semantic": "source", + "value_type": "event_pointer", + "relay_indexed": false + } + ], + "reducers": [ + "knowledge_projection" + ], + "codec_support": { + "encode": true, + "decode": true, + "contract_validation": true, + "verified_decode": true, + "verified_decode_requires_nostr": true + }, + "sdk_builder_support": false, + "deprecated": false, + "replaced_by": null, + "introduced_at": "0.1.0-alpha.2" + }, + { + "contract_id": "radroots.knowledge.review.v1", + "kind": 3462, + "class": "regular", + "standard": "radroots", + "stability": "stable", + "privacy": "public", + "author_role": "any", + "content_schema": "json_object", + "payload_type": "RadrootsKnowledgeReview", + "discriminators": [ + { + "type": "tag_equals", + "name": "contract", + "value": "radroots.knowledge.review.v1" + } + ], + "tag_contracts": [ + { + "name": "contract", + "cardinality": "required_one", + "semantic": "contract", + "value_type": "contract_id", + "relay_indexed": false + }, + { + "name": "review_target", + "cardinality": "required_one", + "semantic": "review_target", + "value_type": "event_pointer", + "relay_indexed": false + }, + { + "name": "evidence", + "cardinality": "optional_many", + "semantic": "evidence", + "value_type": "event_pointer", + "relay_indexed": false + } + ], + "reducers": [ + "knowledge_projection" + ], + "codec_support": { + "encode": true, + "decode": true, + "contract_validation": true, + "verified_decode": true, + "verified_decode_requires_nostr": true + }, + "sdk_builder_support": false, + "deprecated": false, + "replaced_by": null, + "introduced_at": "0.1.0-alpha.2" + }, + { + "contract_id": "radroots.knowledge.source.v1", + "kind": 30450, + "class": "addressable", + "standard": "radroots", + "stability": "stable", + "privacy": "public", + "author_role": "any", + "content_schema": "json_object", + "payload_type": "RadrootsKnowledgeSource", + "discriminators": [ + { + "type": "tag_equals", + "name": "contract", + "value": "radroots.knowledge.source.v1" + } + ], + "tag_contracts": [ + { + "name": "d", + "cardinality": "required_one", + "semantic": "identifier", + "value_type": "d_tag", + "relay_indexed": true + }, + { + "name": "contract", + "cardinality": "required_one", + "semantic": "contract", + "value_type": "contract_id", + "relay_indexed": false + }, + { + "name": "t", + "cardinality": "optional_many", + "semantic": "topic", + "value_type": "text", + "relay_indexed": true + }, + { + "name": "source", + "cardinality": "optional_many", + "semantic": "source", + "value_type": "event_pointer", + "relay_indexed": false + } + ], + "reducers": [ + "knowledge_projection" + ], + "codec_support": { + "encode": true, + "decode": true, + "contract_validation": true, + "verified_decode": true, + "verified_decode_requires_nostr": true + }, + "sdk_builder_support": false, + "deprecated": false, + "replaced_by": null, + "introduced_at": "0.1.0-alpha.2" + }, + { + "contract_id": "radroots.wiki.article.v1", + "kind": 30818, + "class": "addressable", + "standard": "nip54", + "stability": "stable", + "privacy": "public", + "author_role": "any", + "content_schema": "djot", + "payload_type": "RadrootsWikiArticle", + "discriminators": [ + { + "type": "kind_only" + } + ], + "tag_contracts": [ + { + "name": "d", + "cardinality": "required_one", + "semantic": "identifier", + "value_type": "d_tag", + "relay_indexed": true + }, + { + "name": "title", + "cardinality": "optional_one", + "semantic": "title", + "value_type": "text", + "relay_indexed": false + }, + { + "name": "summary", + "cardinality": "optional_one", + "semantic": "summary", + "value_type": "text", + "relay_indexed": false + }, + { + "name": "published_at", + "cardinality": "optional_one", + "semantic": "published_at", + "value_type": "unix_timestamp", + "relay_indexed": false + }, + { + "name": "t", + "cardinality": "optional_many", + "semantic": "topic", + "value_type": "text", + "relay_indexed": true + } + ], + "reducers": [ + "knowledge_projection" + ], + "codec_support": { + "encode": true, + "decode": true, + "contract_validation": true, + "verified_decode": true, + "verified_decode_requires_nostr": true + }, + "sdk_builder_support": false, + "deprecated": false, + "replaced_by": null, + "introduced_at": "0.1.0-alpha.2" + }, + { + "contract_id": "radroots.wiki.merge_request.v1", + "kind": 818, + "class": "regular", + "standard": "nip54", + "stability": "stable", + "privacy": "public", + "author_role": "any", + "content_schema": "json_object", + "payload_type": "RadrootsWikiMergeRequest", + "discriminators": [ + { + "type": "kind_only" + } + ], + "tag_contracts": [ + { + "name": "a", + "cardinality": "required_one", + "semantic": "addressable_coordinate", + "value_type": "addressable_coordinate", + "relay_indexed": true + }, + { + "name": "p", + "cardinality": "required_one", + "semantic": "counterparty", + "value_type": "public_key", + "relay_indexed": true + }, + { + "name": "e", + "cardinality": "required_one", + "semantic": "source", + "value_type": "event_id", + "relay_indexed": true + }, + { + "name": "e", + "cardinality": "optional_one", + "semantic": "previous_event", + "value_type": "event_id", + "relay_indexed": true + } + ], + "reducers": [ + "knowledge_projection" + ], + "codec_support": { + "encode": true, + "decode": true, + "contract_validation": true, + "verified_decode": true, + "verified_decode_requires_nostr": true + }, + "sdk_builder_support": false, + "deprecated": false, + "replaced_by": null, + "introduced_at": "0.1.0-alpha.2" + }, + { + "contract_id": "radroots.wiki.redirect.v1", + "kind": 30819, + "class": "addressable", + "standard": "nip54", + "stability": "stable", + "privacy": "public", + "author_role": "any", + "content_schema": "empty", + "payload_type": "RadrootsWikiRedirect", + "discriminators": [ + { + "type": "kind_only" + } + ], + "tag_contracts": [ + { + "name": "d", + "cardinality": "required_one", + "semantic": "identifier", + "value_type": "d_tag", + "relay_indexed": true + }, + { + "name": "a", + "cardinality": "required_one", + "semantic": "addressable_coordinate", + "value_type": "addressable_coordinate", + "relay_indexed": true + } + ], + "reducers": [ + "knowledge_projection" + ], + "codec_support": { + "encode": true, + "decode": true, + "contract_validation": true, + "verified_decode": true, + "verified_decode_requires_nostr": true + }, + "sdk_builder_support": false, + "deprecated": false, + "replaced_by": null, + "introduced_at": "0.1.0-alpha.2" + } + ] +} diff --git a/contracts/knowledge/knowledge_event_contract_manifest.v1.sha256 b/contracts/knowledge/knowledge_event_contract_manifest.v1.sha256 @@ -0,0 +1 @@ +8e685748e731767c6a81ce0721a674567dd8010e800d21cd5e12688da1b73757 diff --git a/contracts/operations.toml b/contracts/operations.toml @@ -4,7 +4,15 @@ version = "0.1.0-alpha.2" source = "rust" [public] -domains = ["profile", "farm", "listing", "order", "trade_validation", "social"] +domains = [ + "profile", + "farm", + "listing", + "order", + "trade_validation", + "social", + "knowledge", +] [shared_types] public = [ @@ -40,10 +48,29 @@ public = [ "RadrootsOrderInventoryCommitment", "RadrootsOrderDecisionOutcome", "RadrootsOrderDecision", + "RadrootsWikiArticle", + "RadrootsWikiRedirect", + "RadrootsWikiMergeRequest", + "RadrootsKnowledgeSource", + "RadrootsKnowledgeClaim", + "RadrootsKnowledgeRelation", + "RadrootsKnowledgeReview", + "RadrootsKnowledgeFieldReport", + "RadrootsEvidenceBounty", + "RadrootsKnowledgeChangeProposal", + "RadrootsContributionAttestation", + "RadrootsKnowledgeContractManifest", + "RadrootsDecodedEvent", ] [errors] -classes = ["encode_error", "parse_error", "validation_error", "address_error"] +classes = [ + "encode_error", + "parse_error", + "validation_error", + "address_error", + "decode_error", +] [implementation_provenance] model_crates = [ @@ -400,3 +427,47 @@ rust_types = [ [operations.trade_validation_validate_listing_event.conformance] vector = "contracts/conformance/vectors/trade_validation/validate_listing_event.v1.json" + +[operations.knowledge_contract_manifest_json] +domain = "knowledge" +id = "knowledge.contract_manifest_json" +stability = "beta" +inputs = ["RadrootsEventContractRegistry"] +outputs = ["RadrootsKnowledgeContractManifest", "ManifestSha256"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.knowledge_contract_manifest_json.implementation] +rust_modules = ["crates/events_codec/src/manifest.rs"] +rust_types = [ + "radroots_events_codec::manifest::RadrootsKnowledgeContractManifest", +] + +[operations.knowledge_contract_manifest_json.conformance] +vector = "contracts/conformance/vectors/knowledge/manifest_and_decode.v1.json" + +[operations.knowledge_verify_and_decode_event] +domain = "knowledge" +id = "knowledge.verify_and_decode_event" +stability = "beta" +inputs = ["RadrootsNostrEvent"] +outputs = ["RadrootsDecodedEvent"] +error_class = "decode_error" +deterministic = true +signing = "nip01" +transport = "native" + +[operations.knowledge_verify_and_decode_event.implementation] +rust_modules = [ + "crates/events_codec/src/verification.rs", + "crates/events_codec/src/knowledge/decode.rs", +] +rust_types = [ + "radroots_events::RadrootsNostrEvent", + "radroots_events_codec::verification::RadrootsDecodedEvent", +] + +[operations.knowledge_verify_and_decode_event.conformance] +vector = "contracts/conformance/vectors/knowledge/manifest_and_decode.v1.json" diff --git a/crates/events/src/contract.rs b/crates/events/src/contract.rs @@ -3204,8 +3204,28 @@ fn validate_contract_tags( ) -> Result<(), RadrootsContractValidationError> { for tag_contract in contract.tags { let count = tag_count(&event.tags, tag_contract.name); + let has_multiple_contracts_for_name = contract + .tags + .iter() + .filter(|candidate| candidate.name == tag_contract.name) + .count() + > 1; match tag_contract.cardinality { - RadrootsTagCardinality::RequiredOne | RadrootsTagCardinality::RequiredMany => { + RadrootsTagCardinality::RequiredOne => { + if count == 0 { + return Err(RadrootsContractValidationError::MissingTag { + contract_id: contract.id, + name: tag_contract.name, + }); + } + if count != 1 && !has_multiple_contracts_for_name { + return Err(RadrootsContractValidationError::TagCardinalityMismatch { + contract_id: contract.id, + name: tag_contract.name, + }); + } + } + RadrootsTagCardinality::RequiredMany => { if count == 0 { return Err(RadrootsContractValidationError::MissingTag { contract_id: contract.id, @@ -3213,7 +3233,15 @@ fn validate_contract_tags( }); } } - RadrootsTagCardinality::OptionalOne | RadrootsTagCardinality::OptionalMany => {} + RadrootsTagCardinality::OptionalOne => { + if count > 1 && !has_multiple_contracts_for_name { + return Err(RadrootsContractValidationError::TagCardinalityMismatch { + contract_id: contract.id, + name: tag_contract.name, + }); + } + } + RadrootsTagCardinality::OptionalMany => {} } if tag_contract.name == "contract" { let actual = tag_value(&event.tags, "contract").map(ToOwned::to_owned); diff --git a/crates/events/src/lib.rs b/crates/events/src/lib.rs @@ -6,6 +6,8 @@ extern crate alloc; #[cfg(not(feature = "std"))] use alloc::{string::String, vec::Vec}; +pub const RADROOTS_EVENTS_VERSION: &str = env!("CARGO_PKG_VERSION"); + pub mod account; pub mod app_data; pub mod article; diff --git a/crates/events_codec/Cargo.toml b/crates/events_codec/Cargo.toml @@ -19,6 +19,7 @@ serde_json = ["serde", "dep:serde_json"] nostr = ["dep:nostr", "std"] knowledge = ["serde_json", "radroots_events/knowledge"] knowledge-nip54 = ["knowledge", "radroots_events/knowledge-nip54"] +contract-manifest = ["knowledge", "dep:hex", "dep:sha2"] [dependencies] radroots_core = { workspace = true, default-features = false } @@ -30,5 +31,8 @@ serde_json = { workspace = true, default-features = false, features = [ "alloc", ], optional = true } nostr = { workspace = true, optional = true } +hex = { version = "0.4", default-features = false, features = ["alloc"], optional = true } +sha2 = { workspace = true, default-features = false, optional = true } [dev-dependencies] +radroots_test_fixtures = { workspace = true } diff --git a/crates/events_codec/src/farm_crdt/decode.rs b/crates/events_codec/src/farm_crdt/decode.rs @@ -1,8 +1,5 @@ #[cfg(not(feature = "std"))] -use alloc::{ - string::{String, ToString}, - vec::Vec, -}; +use alloc::{string::String, vec::Vec}; use radroots_events::{ RadrootsNostrEvent, diff --git a/crates/events_codec/src/farm_workspace/decode.rs b/crates/events_codec/src/farm_workspace/decode.rs @@ -1,8 +1,5 @@ #[cfg(not(feature = "std"))] -use alloc::{ - string::{String, ToString}, - vec::Vec, -}; +use alloc::{string::String, vec::Vec}; use radroots_events::{ RadrootsNostrEvent, diff --git a/crates/events_codec/src/lib.rs b/crates/events_codec/src/lib.rs @@ -10,6 +10,8 @@ mod field_helpers; pub mod job; #[cfg(feature = "knowledge")] pub mod knowledge; +#[cfg(feature = "contract-manifest")] +pub mod manifest; pub mod parsed; pub mod profile; pub mod report; @@ -57,6 +59,11 @@ mod test_fixtures; #[cfg(feature = "serde_json")] pub mod relay_document; +#[cfg(feature = "contract-manifest")] +pub use manifest::{ + RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION, contract_manifest_json, + contract_manifest_sha256, knowledge_contract_manifest, +}; pub use tag_builders::RadrootsEventTagBuilder; #[cfg(feature = "knowledge")] pub use verification::{ diff --git a/crates/events_codec/src/manifest.rs b/crates/events_codec/src/manifest.rs @@ -0,0 +1,377 @@ +#[cfg(not(feature = "std"))] +use alloc::{ + string::{String, ToString}, + vec, + vec::Vec, +}; + +use radroots_events::RADROOTS_EVENTS_VERSION; +use radroots_events::contract::{ + RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, RadrootsActorRole, RadrootsContentSchema, + RadrootsContractFamily, RadrootsEventClass, RadrootsEventContract, RadrootsEventDiscriminator, + RadrootsEventPrivacy, RadrootsEventStability, RadrootsNostrStandard, RadrootsReducer, + RadrootsTagCardinality, RadrootsTagContract, RadrootsTagSemantic, RadrootsTagValueType, + all_event_contracts, event_contract_family, kind_contract, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +pub const RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION: u32 = 1; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct RadrootsKnowledgeContractManifest { + pub schema_version: u32, + pub registry_version: u32, + pub radroots_events_version: String, + pub radroots_events_codec_version: String, + pub contract_count: usize, + pub contracts: Vec<RadrootsKnowledgeContractManifestEntry>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct RadrootsKnowledgeContractManifestEntry { + pub contract_id: String, + pub kind: u32, + pub class: String, + pub standard: String, + pub stability: String, + pub privacy: String, + pub author_role: String, + pub content_schema: String, + pub payload_type: String, + pub discriminators: Vec<RadrootsKnowledgeManifestDiscriminator>, + pub tag_contracts: Vec<RadrootsKnowledgeManifestTagContract>, + pub reducers: Vec<String>, + pub codec_support: RadrootsKnowledgeManifestCodecSupport, + pub sdk_builder_support: bool, + pub deprecated: bool, + pub replaced_by: Option<String>, + pub introduced_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum RadrootsKnowledgeManifestDiscriminator { + KindOnly, + DTagExact { + value: String, + }, + DTagPrefix { + prefix: String, + }, + DTagSuffix { + suffix: String, + }, + TagEquals { + name: String, + value: String, + }, + ContentJsonFieldEquals { + field: String, + value: String, + }, + EnvelopeType { + value: String, + }, + Composite { + parts: Vec<RadrootsKnowledgeManifestDiscriminator>, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct RadrootsKnowledgeManifestTagContract { + pub name: String, + pub cardinality: String, + pub semantic: String, + pub value_type: String, + pub relay_indexed: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct RadrootsKnowledgeManifestCodecSupport { + pub encode: bool, + pub decode: bool, + pub contract_validation: bool, + pub verified_decode: bool, + pub verified_decode_requires_nostr: bool, +} + +pub fn knowledge_contract_manifest() -> RadrootsKnowledgeContractManifest { + let mut contracts = all_event_contracts() + .iter() + .filter(|contract| { + event_contract_family(contract) == Some(RadrootsContractFamily::Knowledge) + }) + .map(|contract| manifest_entry(contract)) + .collect::<Vec<_>>(); + contracts.sort_by(|left, right| left.contract_id.cmp(&right.contract_id)); + + RadrootsKnowledgeContractManifest { + schema_version: RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION, + registry_version: RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, + radroots_events_version: RADROOTS_EVENTS_VERSION.to_string(), + radroots_events_codec_version: env!("CARGO_PKG_VERSION").to_string(), + contract_count: contracts.len(), + contracts, + } +} + +pub fn contract_manifest_json() -> Result<String, serde_json::Error> { + let mut json = serde_json::to_string_pretty(&knowledge_contract_manifest())?; + json.push('\n'); + Ok(json) +} + +pub fn contract_manifest_sha256() -> Result<String, serde_json::Error> { + let json = contract_manifest_json()?; + Ok(hex::encode(Sha256::digest(json.as_bytes()))) +} + +fn manifest_entry(contract: &RadrootsEventContract) -> RadrootsKnowledgeContractManifestEntry { + let standard = kind_contract(contract.kind) + .map(|contract| standard_label(contract.standard)) + .unwrap_or("unknown"); + + RadrootsKnowledgeContractManifestEntry { + contract_id: contract.id.to_string(), + kind: contract.kind, + class: class_label(contract.class).to_string(), + standard: standard.to_string(), + stability: stability_label(contract.stability).to_string(), + privacy: privacy_label(contract.privacy).to_string(), + author_role: actor_role_label(contract.author_role).to_string(), + content_schema: content_schema_label(contract.content_schema).to_string(), + payload_type: contract.payload_type.to_string(), + discriminators: vec![discriminator_manifest(&contract.discriminator)], + tag_contracts: contract.tags.iter().map(tag_contract_manifest).collect(), + reducers: contract + .reducers + .iter() + .copied() + .map(reducer_label) + .map(ToString::to_string) + .collect(), + codec_support: RadrootsKnowledgeManifestCodecSupport { + encode: true, + decode: true, + contract_validation: true, + verified_decode: true, + verified_decode_requires_nostr: true, + }, + sdk_builder_support: false, + deprecated: false, + replaced_by: None, + introduced_at: env!("CARGO_PKG_VERSION").to_string(), + } +} + +fn discriminator_manifest( + discriminator: &RadrootsEventDiscriminator, +) -> RadrootsKnowledgeManifestDiscriminator { + match discriminator { + RadrootsEventDiscriminator::KindOnly => RadrootsKnowledgeManifestDiscriminator::KindOnly, + RadrootsEventDiscriminator::DTagExact(value) => { + RadrootsKnowledgeManifestDiscriminator::DTagExact { + value: (*value).to_string(), + } + } + RadrootsEventDiscriminator::DTagPrefix(prefix) => { + RadrootsKnowledgeManifestDiscriminator::DTagPrefix { + prefix: (*prefix).to_string(), + } + } + RadrootsEventDiscriminator::DTagSuffix(suffix) => { + RadrootsKnowledgeManifestDiscriminator::DTagSuffix { + suffix: (*suffix).to_string(), + } + } + RadrootsEventDiscriminator::TagEquals { name, value } => { + RadrootsKnowledgeManifestDiscriminator::TagEquals { + name: (*name).to_string(), + value: (*value).to_string(), + } + } + RadrootsEventDiscriminator::ContentJsonFieldEquals { field, value } => { + RadrootsKnowledgeManifestDiscriminator::ContentJsonFieldEquals { + field: (*field).to_string(), + value: (*value).to_string(), + } + } + RadrootsEventDiscriminator::EnvelopeType(value) => { + RadrootsKnowledgeManifestDiscriminator::EnvelopeType { + value: (*value).to_string(), + } + } + RadrootsEventDiscriminator::Composite(parts) => { + RadrootsKnowledgeManifestDiscriminator::Composite { + parts: parts.iter().map(discriminator_manifest).collect(), + } + } + } +} + +fn tag_contract_manifest(contract: &RadrootsTagContract) -> RadrootsKnowledgeManifestTagContract { + RadrootsKnowledgeManifestTagContract { + name: contract.name.to_string(), + cardinality: tag_cardinality_label(contract.cardinality).to_string(), + semantic: tag_semantic_label(contract.semantic).to_string(), + value_type: tag_value_type_label(contract.value_type).to_string(), + relay_indexed: contract.relay_indexed, + } +} + +fn class_label(value: RadrootsEventClass) -> &'static str { + match value { + RadrootsEventClass::Regular => "regular", + RadrootsEventClass::Replaceable => "replaceable", + RadrootsEventClass::Addressable => "addressable", + RadrootsEventClass::Ephemeral => "ephemeral", + } +} + +fn standard_label(value: RadrootsNostrStandard) -> &'static str { + match value { + RadrootsNostrStandard::Nip01 => "nip01", + RadrootsNostrStandard::Nip09 => "nip09", + RadrootsNostrStandard::Nip17 => "nip17", + RadrootsNostrStandard::Nip18 => "nip18", + RadrootsNostrStandard::Nip22 => "nip22", + RadrootsNostrStandard::Nip23 => "nip23", + RadrootsNostrStandard::Nip25 => "nip25", + RadrootsNostrStandard::Nip28 => "nip28", + RadrootsNostrStandard::Nip29 => "nip29", + RadrootsNostrStandard::Nip42 => "nip42", + RadrootsNostrStandard::Nip51 => "nip51", + RadrootsNostrStandard::Nip52 => "nip52", + RadrootsNostrStandard::Nip53 => "nip53", + RadrootsNostrStandard::Nip54 => "nip54", + RadrootsNostrStandard::Nip56 => "nip56", + RadrootsNostrStandard::Nip57 => "nip57", + RadrootsNostrStandard::Nip78 => "nip78", + RadrootsNostrStandard::Nip90 => "nip90", + RadrootsNostrStandard::Nip94 => "nip94", + RadrootsNostrStandard::Nip98 => "nip98", + RadrootsNostrStandard::Radroots => "radroots", + } +} + +fn stability_label(value: RadrootsEventStability) -> &'static str { + match value { + RadrootsEventStability::Stable => "stable", + RadrootsEventStability::Experimental => "experimental", + } +} + +fn privacy_label(value: RadrootsEventPrivacy) -> &'static str { + match value { + RadrootsEventPrivacy::Public => "public", + RadrootsEventPrivacy::Encrypted => "encrypted", + RadrootsEventPrivacy::LocalOnly => "local_only", + RadrootsEventPrivacy::Secret => "secret", + } +} + +fn actor_role_label(value: RadrootsActorRole) -> &'static str { + match value { + RadrootsActorRole::Any => "any", + RadrootsActorRole::Application => "application", + RadrootsActorRole::Buyer => "buyer", + RadrootsActorRole::Farmer => "farmer", + RadrootsActorRole::Member => "member", + RadrootsActorRole::Moderator => "moderator", + RadrootsActorRole::Relay => "relay", + RadrootsActorRole::Seller => "seller", + RadrootsActorRole::Service => "service", + } +} + +fn content_schema_label(value: RadrootsContentSchema) -> &'static str { + match value { + RadrootsContentSchema::Empty => "empty", + RadrootsContentSchema::JsonObject => "json_object", + RadrootsContentSchema::PlainText => "plain_text", + RadrootsContentSchema::Markdown => "markdown", + RadrootsContentSchema::Djot => "djot", + RadrootsContentSchema::Encrypted => "encrypted", + RadrootsContentSchema::BinaryReference => "binary_reference", + } +} + +fn tag_cardinality_label(value: RadrootsTagCardinality) -> &'static str { + match value { + RadrootsTagCardinality::RequiredOne => "required_one", + RadrootsTagCardinality::OptionalOne => "optional_one", + RadrootsTagCardinality::OptionalMany => "optional_many", + RadrootsTagCardinality::RequiredMany => "required_many", + } +} + +fn tag_semantic_label(value: RadrootsTagSemantic) -> &'static str { + match value { + RadrootsTagSemantic::AddressableCoordinate => "addressable_coordinate", + RadrootsTagSemantic::Category => "category", + RadrootsTagSemantic::Citation => "citation", + RadrootsTagSemantic::Contract => "contract", + RadrootsTagSemantic::Counterparty => "counterparty", + RadrootsTagSemantic::Evidence => "evidence", + RadrootsTagSemantic::EventPointer => "event_pointer", + RadrootsTagSemantic::Geohash => "geohash", + RadrootsTagSemantic::GroupId => "group_id", + RadrootsTagSemantic::Identifier => "identifier", + RadrootsTagSemantic::Image => "image", + RadrootsTagSemantic::Kind => "kind", + RadrootsTagSemantic::ListingAddress => "listing_address", + RadrootsTagSemantic::ListingSnapshot => "listing_snapshot", + RadrootsTagSemantic::Location => "location", + RadrootsTagSemantic::PreviousEvent => "previous_event", + RadrootsTagSemantic::Price => "price", + RadrootsTagSemantic::PublishedAt => "published_at", + RadrootsTagSemantic::Relay => "relay", + RadrootsTagSemantic::ReviewTarget => "review_target", + RadrootsTagSemantic::RootEvent => "root_event", + RadrootsTagSemantic::ServiceInput => "service_input", + RadrootsTagSemantic::ServiceOutput => "service_output", + RadrootsTagSemantic::Source => "source", + RadrootsTagSemantic::Status => "status", + RadrootsTagSemantic::Summary => "summary", + RadrootsTagSemantic::Title => "title", + RadrootsTagSemantic::Topic => "topic", + RadrootsTagSemantic::Url => "url", + } +} + +fn tag_value_type_label(value: RadrootsTagValueType) -> &'static str { + match value { + RadrootsTagValueType::AddressableCoordinate => "addressable_coordinate", + RadrootsTagValueType::ContractId => "contract_id", + RadrootsTagValueType::DTag => "d_tag", + RadrootsTagValueType::EventId => "event_id", + RadrootsTagValueType::EventPointer => "event_pointer", + RadrootsTagValueType::Geohash => "geohash", + RadrootsTagValueType::Kind => "kind", + RadrootsTagValueType::PublicKey => "public_key", + RadrootsTagValueType::RelayUrl => "relay_url", + RadrootsTagValueType::Sha256 => "sha256", + RadrootsTagValueType::Text => "text", + RadrootsTagValueType::UnixTimestamp => "unix_timestamp", + RadrootsTagValueType::Url => "url", + RadrootsTagValueType::Uuid => "uuid", + } +} + +fn reducer_label(value: RadrootsReducer) -> &'static str { + match value { + RadrootsReducer::CalendarProjection => "calendar_projection", + RadrootsReducer::FarmOpsProjection => "farm_ops_projection", + RadrootsReducer::GroupProjection => "group_projection", + RadrootsReducer::KnowledgeProjection => "knowledge_projection", + RadrootsReducer::ListingInventoryAccounting => "listing_inventory_accounting", + RadrootsReducer::ListingProjection => "listing_projection", + RadrootsReducer::MarketProjection => "market_projection", + RadrootsReducer::OrderProjection => "order_projection", + RadrootsReducer::ProfileProjection => "profile_projection", + RadrootsReducer::RelayPolicyProjection => "relay_policy_projection", + RadrootsReducer::SocialProjection => "social_projection", + RadrootsReducer::TradeValidation => "trade_validation", + } +} diff --git a/crates/events_codec/src/order/decode.rs b/crates/events_codec/src/order/decode.rs @@ -1,5 +1,5 @@ #[cfg(all(not(feature = "std"), feature = "serde_json"))] -use alloc::{borrow::ToOwned, format, string::String, vec::Vec}; +use alloc::{string::String, vec::Vec}; #[cfg(feature = "serde_json")] use radroots_events::{ diff --git a/crates/events_codec/src/order/encode.rs b/crates/events_codec/src/order/encode.rs @@ -1,6 +1,3 @@ -#[cfg(all(not(feature = "std"), feature = "serde_json"))] -use alloc::string::String; - #[cfg(feature = "serde_json")] use radroots_events::{ RadrootsNostrEventPtr, diff --git a/crates/events_codec/src/verification.rs b/crates/events_codec/src/verification.rs @@ -1,8 +1,5 @@ #[cfg(not(feature = "std"))] -use alloc::{ - string::{String, ToString}, - vec::Vec, -}; +use alloc::string::{String, ToString}; use core::fmt; #[cfg(feature = "nostr")] diff --git a/crates/events_codec/tests/knowledge_fixtures.rs b/crates/events_codec/tests/knowledge_fixtures.rs @@ -0,0 +1,361 @@ +#![cfg(all(feature = "knowledge", feature = "nostr"))] + +use std::collections::BTreeSet; + +use radroots_events::RadrootsNostrEvent; +use radroots_events::contract::{ + RadrootsContractValidationError, RadrootsEventClass, all_event_contracts, + validate_event_contract_shape, +}; +use radroots_events::kinds::{ + KIND_CONTRIBUTION_ATTESTATION, KIND_KNOWLEDGE_CHANGE_PROPOSAL, KIND_KNOWLEDGE_CLAIM, + KIND_KNOWLEDGE_FIELD_REPORT, KIND_KNOWLEDGE_RELATION, KIND_KNOWLEDGE_REVIEW, +}; +use radroots_events::knowledge::{ + RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, RadrootsWikiArticle, +}; +use radroots_events_codec::error::EventEncodeError; +use radroots_events_codec::knowledge::{ + contribution_attestation_to_wire_parts, evidence_bounty_to_wire_parts, + knowledge_change_proposal_to_wire_parts, knowledge_claim_to_wire_parts, + knowledge_field_report_to_wire_parts, knowledge_relation_to_wire_parts, + knowledge_review_to_wire_parts, knowledge_source_to_wire_parts, wiki_article_to_wire_parts, + wiki_merge_request_to_wire_parts, wiki_redirect_to_wire_parts, +}; +use radroots_events_codec::verification::{ + RadrootsDecodeError, RadrootsDecodedEvent, RadrootsNip01VerificationError, + verify_and_decode_radroots_event, +}; +use radroots_events_codec::wire::WireEventParts; +use radroots_test_fixtures::knowledge::{ + RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES, RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS, + RadrootsKnowledgeFixture, hex_64, knowledge_claim, knowledge_field_report, + knowledge_valid_fixtures, wiki_article, +}; + +fn event_from_parts(parts: WireEventParts) -> RadrootsNostrEvent { + RadrootsNostrEvent { + id: hex_64('0'), + author: hex_64('a'), + created_at: 1_800_000_000, + kind: parts.kind, + tags: parts.tags, + content: parts.content, + sig: "1".repeat(128), + } +} + +fn sign_parts(parts: WireEventParts) -> RadrootsNostrEvent { + let tags = parts + .tags + .into_iter() + .map(nostr::Tag::parse) + .collect::<Result<Vec<_>, _>>() + .expect("tags"); + let keys = + nostr::Keys::parse("0101010101010101010101010101010101010101010101010101010101010101") + .expect("keys"); + let event = nostr::EventBuilder::new(nostr::Kind::Custom(parts.kind as u16), parts.content) + .tags(tags) + .custom_created_at(nostr::Timestamp::from_secs(1_800_000_000)) + .sign_with_keys(&keys) + .expect("signed event"); + RadrootsNostrEvent { + id: event.id.to_hex(), + author: event.pubkey.to_hex(), + created_at: event.created_at.as_secs() as u32, + kind: u32::from(event.kind.as_u16()), + tags: event + .tags + .as_slice() + .iter() + .map(|tag| tag.as_slice().to_vec()) + .collect(), + content: event.content, + sig: event.sig.to_string(), + } +} + +fn parts_for_fixture(fixture: &RadrootsKnowledgeFixture) -> WireEventParts { + match fixture { + RadrootsKnowledgeFixture::WikiArticle(value) => wiki_article_to_wire_parts(value).unwrap(), + RadrootsKnowledgeFixture::WikiRedirect(value) => { + wiki_redirect_to_wire_parts(value).unwrap() + } + RadrootsKnowledgeFixture::WikiMergeRequest(value) => { + wiki_merge_request_to_wire_parts(value).unwrap() + } + RadrootsKnowledgeFixture::KnowledgeSource(value) => { + knowledge_source_to_wire_parts(value).unwrap() + } + RadrootsKnowledgeFixture::KnowledgeClaim(value) => { + knowledge_claim_to_wire_parts(value).unwrap() + } + RadrootsKnowledgeFixture::KnowledgeRelation(value) => { + knowledge_relation_to_wire_parts(value).unwrap() + } + RadrootsKnowledgeFixture::KnowledgeReview(value) => { + knowledge_review_to_wire_parts(value).unwrap() + } + RadrootsKnowledgeFixture::KnowledgeFieldReport(value) => { + knowledge_field_report_to_wire_parts(value).unwrap() + } + RadrootsKnowledgeFixture::EvidenceBounty(value) => { + evidence_bounty_to_wire_parts(value).unwrap() + } + RadrootsKnowledgeFixture::KnowledgeChangeProposal(value) => { + knowledge_change_proposal_to_wire_parts(value).unwrap() + } + RadrootsKnowledgeFixture::ContributionAttestation(value) => { + contribution_attestation_to_wire_parts(value).unwrap() + } + } +} + +#[test] +fn golden_knowledge_fixtures_cover_every_contract() { + let fixtures = knowledge_valid_fixtures(); + let fixture_contracts = fixtures + .iter() + .map(|fixture| fixture.contract_id) + .collect::<BTreeSet<_>>(); + let registry_contracts = all_event_contracts() + .iter() + .filter(|contract| { + RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS + .iter() + .any(|contract_id| *contract_id == contract.id) + }) + .map(|contract| contract.id) + .collect::<BTreeSet<_>>(); + + assert_eq!(fixture_contracts, registry_contracts); + + for fixture in fixtures { + let event = event_from_parts(parts_for_fixture(&fixture.data)); + validate_event_contract_shape(&event, fixture.contract_id).unwrap(); + assert_eq!(event.kind, fixture.kind, "{}", fixture.id); + } +} + +#[test] +fn adversarial_knowledge_fixtures_reject_at_expected_stages() { + let malformed = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES + .iter() + .find(|fixture| fixture.id == "malformed_tags") + .unwrap(); + let mut malformed_event = + event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); + malformed_event.tags.push(vec![ + "contract".to_string(), + RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(), + ]); + let error = validate_event_contract_shape(&malformed_event, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA) + .unwrap_err(); + assert_eq!(malformed.pipeline_stage, "contract_validation"); + assert_eq!(malformed.expected_error_code, error.code()); + + let wrong_schema = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES + .iter() + .find(|fixture| fixture.id == "wrong_schema") + .unwrap(); + let mut wrong_schema_event = + event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); + let mut wrong_schema_value: serde_json::Value = + serde_json::from_str(&wrong_schema_event.content).unwrap(); + wrong_schema_value["schema"] = serde_json::Value::from("radroots.knowledge.relation.v1"); + wrong_schema_event.content = serde_json::to_string(&wrong_schema_value).unwrap(); + let error = validate_event_contract_shape(&wrong_schema_event, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA) + .unwrap_err(); + assert_eq!(wrong_schema.pipeline_stage, "contract_validation"); + assert_eq!(wrong_schema.expected_error_code, error.code()); + + let missing_contract = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES + .iter() + .find(|fixture| fixture.id == "missing_contract_id") + .unwrap(); + let mut missing_contract_event = + event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); + missing_contract_event + .tags + .retain(|tag| tag.first().map(|value| value.as_str()) != Some("contract")); + let signed = sign_parts(WireEventParts { + kind: missing_contract_event.kind, + content: missing_contract_event.content, + tags: missing_contract_event.tags, + }); + let error = verify_and_decode_radroots_event(signed).unwrap_err(); + assert_eq!(missing_contract.pipeline_stage, error.code()); + assert!(matches!(error, RadrootsDecodeError::ContractValidation(_))); + + let private_coordinates = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES + .iter() + .find(|fixture| fixture.id == "private_coordinate_leakage") + .unwrap(); + let mut private_event = + event_from_parts(knowledge_field_report_to_wire_parts(&knowledge_field_report()).unwrap()); + let mut private_value: serde_json::Value = + serde_json::from_str(&private_event.content).unwrap(); + private_value["context"]["latitude"] = serde_json::Value::from("45.0000"); + private_event.content = serde_json::to_string(&private_value).unwrap(); + let signed = sign_parts(WireEventParts { + kind: private_event.kind, + content: private_event.content, + tags: private_event.tags, + }); + let error = verify_and_decode_radroots_event(signed).unwrap_err(); + assert_eq!(private_coordinates.pipeline_stage, error.code()); + match error { + RadrootsDecodeError::EventParse(error) => { + assert_eq!(private_coordinates.expected_error_code, error.code()); + } + error => panic!("{error:?}"), + } + + let unsupported = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES + .iter() + .find(|fixture| fixture.id == "unsupported_contract_shape") + .unwrap(); + let mut unsupported_event = + event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); + for tag in &mut unsupported_event.tags { + if tag.first().map(|value| value.as_str()) == Some("contract") { + tag[1] = "radroots.knowledge.unsupported.v1".to_string(); + } + } + let signed = sign_parts(WireEventParts { + kind: unsupported_event.kind, + content: unsupported_event.content, + tags: unsupported_event.tags, + }); + let error = verify_and_decode_radroots_event(signed).unwrap_err(); + assert_eq!(unsupported.pipeline_stage, error.code()); + assert!(matches!(error, RadrootsDecodeError::ContractValidation(_))); +} + +#[test] +fn nip54_and_signature_adversarial_fixtures_are_rejected() { + let invalid_d_tag = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES + .iter() + .find(|fixture| fixture.id == "invalid_nip54_d_tag") + .unwrap(); + let mut article: RadrootsWikiArticle = wiki_article(); + article.d_tag = "Soil Health".to_string(); + let error = wiki_article_to_wire_parts(&article).unwrap_err(); + assert_eq!(invalid_d_tag.pipeline_stage, "encode"); + assert_eq!(invalid_d_tag.expected_error_code, error.code()); + assert!(matches!(error, EventEncodeError::InvalidField("d_tag"))); + + let id_mismatch = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES + .iter() + .find(|fixture| fixture.id == "id_mismatch") + .unwrap(); + let signed = sign_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); + let mut mutated = signed.clone(); + mutated.content = mutated.content.replace("Cover crops", "Compost"); + let error = verify_and_decode_radroots_event(mutated).unwrap_err(); + assert_eq!(id_mismatch.pipeline_stage, error.code()); + match error { + RadrootsDecodeError::Nip01Verification(RadrootsNip01VerificationError::IdMismatch { + .. + }) => {} + error => panic!("{error:?}"), + } + + let signature_invalidity = RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES + .iter() + .find(|fixture| fixture.id == "signature_invalidity") + .unwrap(); + let mut bad_signature = signed; + bad_signature.sig = "0".repeat(128); + let error = verify_and_decode_radroots_event(bad_signature).unwrap_err(); + assert_eq!(signature_invalidity.pipeline_stage, error.code()); + match error { + RadrootsDecodeError::Nip01Verification( + RadrootsNip01VerificationError::SignatureInvalid, + ) => {} + error => panic!("{error:?}"), + } +} + +#[test] +fn authoritative_knowledge_status_fields_are_rejected() { + for field in [ + "review_status", + "canon_status", + "approved_for_canon", + "rights_status", + "trust_status", + "trusted", + ] { + let mut event = + event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); + let mut value: serde_json::Value = serde_json::from_str(&event.content).unwrap(); + value[field] = serde_json::Value::from("approved"); + event.content = serde_json::to_string(&value).unwrap(); + let error = + validate_event_contract_shape(&event, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA).unwrap_err(); + assert_eq!( + error, + RadrootsContractValidationError::ForbiddenContentField { + contract_id: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, + field, + } + ); + } +} + +#[test] +fn immutable_knowledge_contracts_are_regular_events() { + let regular_ids = [ + (RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, KIND_KNOWLEDGE_CLAIM), + ("radroots.knowledge.relation.v1", KIND_KNOWLEDGE_RELATION), + ("radroots.knowledge.review.v1", KIND_KNOWLEDGE_REVIEW), + ( + RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, + KIND_KNOWLEDGE_FIELD_REPORT, + ), + ( + "radroots.knowledge.change_proposal.v1", + KIND_KNOWLEDGE_CHANGE_PROPOSAL, + ), + ( + "radroots.knowledge.contribution_attestation.v1", + KIND_CONTRIBUTION_ATTESTATION, + ), + ]; + for (contract_id, kind) in regular_ids { + let contract = all_event_contracts() + .iter() + .find(|contract| contract.id == contract_id) + .unwrap(); + assert_eq!(contract.kind, kind); + assert_eq!(contract.class, RadrootsEventClass::Regular); + } +} + +#[test] +fn verified_decode_exposes_representative_downstream_compatibility_events() { + let fixture_ids = [ + "wiki_article_valid", + "knowledge_source_valid", + "knowledge_claim_valid", + "knowledge_review_valid", + "knowledge_field_report_valid", + ]; + for fixture in knowledge_valid_fixtures() + .into_iter() + .filter(|fixture| fixture_ids.contains(&fixture.id)) + { + let signed = sign_parts(parts_for_fixture(&fixture.data)); + let decoded = verify_and_decode_radroots_event(signed).unwrap(); + match decoded { + RadrootsDecodedEvent::WikiArticle(_) + | RadrootsDecodedEvent::KnowledgeSource(_) + | RadrootsDecodedEvent::KnowledgeClaim(_) + | RadrootsDecodedEvent::KnowledgeReview(_) + | RadrootsDecodedEvent::KnowledgeFieldReport(_) => {} + decoded => panic!("{decoded:?}"), + } + } +} diff --git a/crates/events_codec/tests/knowledge_manifest.rs b/crates/events_codec/tests/knowledge_manifest.rs @@ -0,0 +1,164 @@ +#![cfg(all(feature = "knowledge", feature = "contract-manifest"))] + +use std::collections::BTreeSet; + +use radroots_events::RADROOTS_EVENTS_VERSION; +use radroots_events::contract::{ + RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, RadrootsEventClass, all_event_contracts, +}; +use radroots_events::kinds::{ + KIND_CONTRIBUTION_ATTESTATION, KIND_KNOWLEDGE_CHANGE_PROPOSAL, KIND_KNOWLEDGE_CLAIM, + KIND_KNOWLEDGE_FIELD_REPORT, KIND_KNOWLEDGE_RELATION, KIND_KNOWLEDGE_REVIEW, +}; +use radroots_events::knowledge::{ + RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, +}; +use radroots_events_codec::manifest::{ + RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION, RadrootsKnowledgeContractManifest, + contract_manifest_json, contract_manifest_sha256, knowledge_contract_manifest, +}; +use radroots_test_fixtures::knowledge::RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS; + +const MANIFEST_JSON: &str = + include_str!("../../../contracts/knowledge/knowledge_event_contract_manifest.v1.json"); +const MANIFEST_SHA256: &str = + include_str!("../../../contracts/knowledge/knowledge_event_contract_manifest.v1.sha256"); +const REGENPROTO_COMPATIBILITY_VECTOR: &str = include_str!( + "../../../contracts/conformance/vectors/knowledge/regenproto_compatibility.v1.json" +); + +#[test] +fn knowledge_manifest_is_deterministic_and_matches_artifacts() { + let first = contract_manifest_json().unwrap(); + let second = contract_manifest_json().unwrap(); + assert_eq!(first, second); + assert_eq!(first, MANIFEST_JSON); + + let first_hash = contract_manifest_sha256().unwrap(); + let second_hash = contract_manifest_sha256().unwrap(); + assert_eq!(first_hash, second_hash); + assert_eq!(format!("{first_hash}\n"), MANIFEST_SHA256); +} + +#[test] +fn knowledge_manifest_covers_required_fields_for_every_contract() { + let manifest: RadrootsKnowledgeContractManifest = serde_json::from_str(MANIFEST_JSON).unwrap(); + assert_eq!( + manifest.schema_version, + RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION + ); + assert_eq!( + manifest.registry_version, + RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION + ); + assert_eq!(manifest.radroots_events_version, RADROOTS_EVENTS_VERSION); + assert_eq!( + manifest.radroots_events_codec_version, + env!("CARGO_PKG_VERSION") + ); + assert_eq!(manifest.contract_count, manifest.contracts.len()); + + let mut sorted = manifest + .contracts + .iter() + .map(|contract| contract.contract_id.as_str()) + .collect::<Vec<_>>(); + let original = sorted.clone(); + sorted.sort_unstable(); + assert_eq!(original, sorted); + + for contract in &manifest.contracts { + assert!(!contract.contract_id.trim().is_empty()); + assert!(contract.kind > 0); + assert!(!contract.class.trim().is_empty()); + assert!(!contract.standard.trim().is_empty()); + assert!(!contract.stability.trim().is_empty()); + assert!(!contract.privacy.trim().is_empty()); + assert!(!contract.content_schema.trim().is_empty()); + assert!(!contract.payload_type.trim().is_empty()); + assert!(!contract.discriminators.is_empty()); + assert!( + !contract.codec_support.verified_decode_requires_nostr + || contract.codec_support.verified_decode + ); + assert!(contract.codec_support.encode); + assert!(contract.codec_support.decode); + assert!(contract.codec_support.contract_validation); + assert!(!contract.deprecated); + assert!(contract.replaced_by.is_none()); + assert!(!contract.introduced_at.trim().is_empty()); + } + + let manifest_contracts = manifest + .contracts + .iter() + .map(|contract| contract.contract_id.as_str()) + .collect::<BTreeSet<_>>(); + let fixture_contracts = RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS + .iter() + .copied() + .collect::<BTreeSet<_>>(); + assert_eq!(manifest_contracts, fixture_contracts); +} + +#[test] +fn knowledge_manifest_preserves_regular_immutable_classification() { + let manifest = knowledge_contract_manifest(); + for (contract_id, kind) in [ + (RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, KIND_KNOWLEDGE_CLAIM), + ("radroots.knowledge.relation.v1", KIND_KNOWLEDGE_RELATION), + ("radroots.knowledge.review.v1", KIND_KNOWLEDGE_REVIEW), + ( + RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, + KIND_KNOWLEDGE_FIELD_REPORT, + ), + ( + "radroots.knowledge.change_proposal.v1", + KIND_KNOWLEDGE_CHANGE_PROPOSAL, + ), + ( + "radroots.knowledge.contribution_attestation.v1", + KIND_CONTRIBUTION_ATTESTATION, + ), + ] { + let manifest_entry = manifest + .contracts + .iter() + .find(|contract| contract.contract_id == contract_id) + .unwrap(); + let registry_entry = all_event_contracts() + .iter() + .find(|contract| contract.id == contract_id) + .unwrap(); + assert_eq!(manifest_entry.kind, kind); + assert_eq!(manifest_entry.class, "regular"); + assert_eq!(registry_entry.class, RadrootsEventClass::Regular); + } +} + +#[test] +fn regenproto_compatibility_vector_stays_generalized() { + let value: serde_json::Value = serde_json::from_str(REGENPROTO_COMPATIBILITY_VECTOR).unwrap(); + assert_eq!(value["suite"], "knowledge_downstream_compatibility"); + let ids = value["vectors"] + .as_array() + .unwrap() + .iter() + .map(|vector| vector["id"].as_str().unwrap()) + .collect::<BTreeSet<_>>(); + assert_eq!( + ids, + [ + "knowledge_claim_downstream_valid_002", + "knowledge_field_report_downstream_valid_004", + "knowledge_review_downstream_valid_003", + "knowledge_source_downstream_valid_001", + "wiki_article_downstream_valid_005", + ] + .into_iter() + .collect() + ); + for forbidden in ["dao", "token", "reputation", "score", "canon_synthesis"] { + assert!(!REGENPROTO_COMPATIBILITY_VECTOR.contains(forbidden)); + } +} diff --git a/crates/test_fixtures/Cargo.toml b/crates/test_fixtures/Cargo.toml @@ -13,3 +13,11 @@ authors = ["Tyson Lupul <tyson@radroots.org>"] [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } + +[dependencies] +radroots_events = { workspace = true, features = [ + "std", + "serde", + "knowledge", + "knowledge-nip54", +] } diff --git a/crates/test_fixtures/src/knowledge.rs b/crates/test_fixtures/src/knowledge.rs @@ -0,0 +1,462 @@ +use radroots_events::RadrootsNostrEventRef; +use radroots_events::kinds::{ + KIND_CONTRIBUTION_ATTESTATION, KIND_EVIDENCE_BOUNTY, KIND_FILE_METADATA, + KIND_KNOWLEDGE_CHANGE_PROPOSAL, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_FIELD_REPORT, + KIND_KNOWLEDGE_RELATION, KIND_KNOWLEDGE_REVIEW, KIND_KNOWLEDGE_SOURCE, KIND_WIKI_ARTICLE, + KIND_WIKI_MERGE_REQUEST, KIND_WIKI_REDIRECT, +}; +use radroots_events::knowledge::{ + RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA, RADROOTS_EVIDENCE_BOUNTY_SCHEMA, + RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, + RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, RADROOTS_KNOWLEDGE_RELATION_SCHEMA, + RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, RadrootsAddressableRef, RadrootsContributionAttestation, + RadrootsEvidenceBounty, RadrootsKnowledgeChangeProposal, RadrootsKnowledgeCitationSpan, + RadrootsKnowledgeClaim, RadrootsKnowledgeFieldContext, RadrootsKnowledgeFieldReport, + RadrootsKnowledgeLocation, RadrootsKnowledgeLocationPrecision, RadrootsKnowledgeNodeRef, + RadrootsKnowledgeObservation, RadrootsKnowledgeObservationValue, RadrootsKnowledgeRelation, + RadrootsKnowledgeReview, RadrootsKnowledgeReviewScope, RadrootsKnowledgeReviewScore, + RadrootsKnowledgeReviewTarget, RadrootsKnowledgeSource, RadrootsWikiArticle, + RadrootsWikiMergeRequest, RadrootsWikiRedirect, +}; + +use crate::RELAY_PRIMARY_WSS; + +pub const RADROOTS_KNOWLEDGE_FIXTURE_NAMESPACE: &str = "radroots-knowledge-fixture-v1"; + +pub const RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS: [&str; 11] = [ + "radroots.wiki.article.v1", + "radroots.wiki.redirect.v1", + "radroots.wiki.merge_request.v1", + RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, + RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, + RADROOTS_KNOWLEDGE_RELATION_SCHEMA, + RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, + RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, + RADROOTS_EVIDENCE_BOUNTY_SCHEMA, + RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA, + RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA, +]; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RadrootsKnowledgeAdversarialFixture { + pub id: &'static str, + pub pipeline_stage: &'static str, + pub expected_error_code: &'static str, +} + +pub const RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES: [RadrootsKnowledgeAdversarialFixture; 8] = [ + RadrootsKnowledgeAdversarialFixture { + id: "malformed_tags", + pipeline_stage: "contract_validation", + expected_error_code: "tag_cardinality_mismatch", + }, + RadrootsKnowledgeAdversarialFixture { + id: "wrong_schema", + pipeline_stage: "contract_validation", + expected_error_code: "content_field_mismatch", + }, + RadrootsKnowledgeAdversarialFixture { + id: "missing_contract_id", + pipeline_stage: "contract_validation", + expected_error_code: "contract_match", + }, + RadrootsKnowledgeAdversarialFixture { + id: "private_coordinate_leakage", + pipeline_stage: "event_parse", + expected_error_code: "invalid_json", + }, + RadrootsKnowledgeAdversarialFixture { + id: "unsupported_contract_shape", + pipeline_stage: "contract_validation", + expected_error_code: "contract_match", + }, + RadrootsKnowledgeAdversarialFixture { + id: "invalid_nip54_d_tag", + pipeline_stage: "encode", + expected_error_code: "invalid_field", + }, + RadrootsKnowledgeAdversarialFixture { + id: "id_mismatch", + pipeline_stage: "nip01_verification", + expected_error_code: "id_mismatch", + }, + RadrootsKnowledgeAdversarialFixture { + id: "signature_invalidity", + pipeline_stage: "nip01_verification", + expected_error_code: "signature_invalid", + }, +]; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsKnowledgeFixture { + WikiArticle(RadrootsWikiArticle), + WikiRedirect(RadrootsWikiRedirect), + WikiMergeRequest(RadrootsWikiMergeRequest), + KnowledgeSource(RadrootsKnowledgeSource), + KnowledgeClaim(RadrootsKnowledgeClaim), + KnowledgeRelation(RadrootsKnowledgeRelation), + KnowledgeReview(RadrootsKnowledgeReview), + KnowledgeFieldReport(RadrootsKnowledgeFieldReport), + EvidenceBounty(RadrootsEvidenceBounty), + KnowledgeChangeProposal(RadrootsKnowledgeChangeProposal), + ContributionAttestation(RadrootsContributionAttestation), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsKnowledgeFixtureCase { + pub id: &'static str, + pub contract_id: &'static str, + pub kind: u32, + pub data: RadrootsKnowledgeFixture, +} + +pub fn hex_64(character: char) -> String { + core::iter::repeat_n(character, 64).collect() +} + +pub fn event_ref(character: char, kind: u32) -> RadrootsNostrEventRef { + RadrootsNostrEventRef { + id: hex_64(character), + author: hex_64('a'), + kind, + d_tag: None, + relays: Some(vec![RELAY_PRIMARY_WSS.to_string()]), + } +} + +pub fn wiki_article_ref() -> RadrootsNostrEventRef { + RadrootsNostrEventRef { + id: hex_64('b'), + author: hex_64('a'), + kind: KIND_WIKI_ARTICLE, + d_tag: Some("soil-health".to_string()), + relays: Some(vec![RELAY_PRIMARY_WSS.to_string()]), + } +} + +pub fn address_ref() -> RadrootsAddressableRef { + RadrootsAddressableRef { + kind: KIND_WIKI_ARTICLE, + pubkey: hex_64('a'), + d_tag: "soil-health".to_string(), + relays: vec![RELAY_PRIMARY_WSS.to_string()], + } +} + +pub fn wiki_article() -> RadrootsWikiArticle { + RadrootsWikiArticle { + d_tag: "soil-health".to_string(), + title: "Soil health".to_string(), + content_djot: "# Soil health\n\nLiving soil supports resilient local food systems." + .to_string(), + summary: Some("Living soil basics".to_string()), + topics: vec!["soil".to_string(), "local-food".to_string()], + references: vec![event_ref('2', KIND_KNOWLEDGE_SOURCE)], + forked_from: Vec::new(), + deferred_to: None, + } +} + +pub fn wiki_redirect() -> RadrootsWikiRedirect { + RadrootsWikiRedirect { + d_tag: "soil".to_string(), + target: wiki_article_ref(), + } +} + +pub fn wiki_merge_request() -> RadrootsWikiMergeRequest { + RadrootsWikiMergeRequest { + target_article: address_ref(), + destination_pubkey: hex_64('a'), + base_version_event_id: Some(hex_64('e')), + source_version_event_id: hex_64('f'), + explanation: Some("Merge synthetic soil article updates".to_string()), + } +} + +pub fn knowledge_source() -> RadrootsKnowledgeSource { + RadrootsKnowledgeSource { + schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + d_tag: "soil-source".to_string(), + title: "Soil Source".to_string(), + source_type: "book".to_string(), + authors: vec!["A. Example".to_string()], + publisher: Some("Radroots Synthetic Press".to_string()), + publication_year: Some(2026), + edition: None, + canonical_url: Some("https://source.example.test/soil-source".to_string()), + artifact_refs: vec![event_ref('3', KIND_FILE_METADATA)], + author_asserted_rights: None, + topics: vec!["soil".to_string()], + summary: Some("Synthetic source for knowledge fixture coverage".to_string()), + } +} + +pub fn knowledge_claim() -> RadrootsKnowledgeClaim { + RadrootsKnowledgeClaim { + schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + claim_type: "practice_effect".to_string(), + text: "Cover crops improve soil structure.".to_string(), + citation_spans: vec![RadrootsKnowledgeCitationSpan { + source_ref: event_ref('4', KIND_KNOWLEDGE_SOURCE), + artifact_ref: None, + page_start: Some(12), + page_end: Some(13), + section_path: vec!["chapter-1".to_string()], + quote_hash: Some(hex_64('5')), + chunk_id: Some("chunk-1".to_string()), + }], + topics: vec!["cover-crops".to_string()], + applies_to: vec!["local-food".to_string()], + author_asserted_confidence: Some("medium".to_string()), + supersedes: Vec::new(), + } +} + +pub fn knowledge_node_ref(label: &str) -> RadrootsKnowledgeNodeRef { + RadrootsKnowledgeNodeRef { + node_type: "event".to_string(), + event_ref: Some(event_ref('6', KIND_KNOWLEDGE_CLAIM)), + address_ref: None, + external_id: None, + label: Some(label.to_string()), + } +} + +pub fn knowledge_relation() -> RadrootsKnowledgeRelation { + RadrootsKnowledgeRelation { + schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + subject: knowledge_node_ref("cover crops"), + predicate: "supports".to_string(), + object: knowledge_node_ref("soil structure"), + support_refs: vec![event_ref('7', KIND_KNOWLEDGE_CLAIM)], + author_asserted_confidence: Some("medium".to_string()), + supersedes: Vec::new(), + } +} + +pub fn knowledge_review() -> RadrootsKnowledgeReview { + RadrootsKnowledgeReview { + schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + target: RadrootsKnowledgeReviewTarget { + event_id: hex_64('8'), + author_pubkey: hex_64('a'), + kind: KIND_KNOWLEDGE_CLAIM, + address: None, + relays: vec![RELAY_PRIMARY_WSS.to_string()], + review_scope: RadrootsKnowledgeReviewScope::SpecificVersion, + }, + reviewer_role: "peer".to_string(), + verdict: "needs_more_evidence".to_string(), + scores: vec![RadrootsKnowledgeReviewScore { + dimension: "evidence".to_string(), + value: "partial".to_string(), + note: None, + }], + notes: Some("Synthetic review".to_string()), + evidence_refs: vec![event_ref('9', KIND_KNOWLEDGE_SOURCE)], + } +} + +pub fn knowledge_field_report() -> RadrootsKnowledgeFieldReport { + RadrootsKnowledgeFieldReport { + schema: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + report_type: "observation".to_string(), + title: "Field observation".to_string(), + summary: Some("Observed cover crop residue.".to_string()), + context: RadrootsKnowledgeFieldContext { + location_precision: RadrootsKnowledgeLocationPrecision::CoarseGeohash, + public_location: Some(RadrootsKnowledgeLocation { + label: Some("watershed".to_string()), + region: Some("synthetic-region".to_string()), + locality: None, + geohash: Some("c23".to_string()), + }), + private_location_ref: None, + topics: vec!["field".to_string()], + context_tags: vec!["observation".to_string()], + }, + observations: vec![RadrootsKnowledgeObservation { + observation_type: "residue".to_string(), + text: "Residue was visible across beds.".to_string(), + observed_at: Some("2026-07-05".to_string()), + values: vec![RadrootsKnowledgeObservationValue { + key: "coverage".to_string(), + value: "medium".to_string(), + unit: None, + }], + }], + artifact_refs: vec![event_ref('c', KIND_FILE_METADATA)], + related_refs: vec![event_ref('d', KIND_KNOWLEDGE_CLAIM)], + limitations: vec!["single observer".to_string()], + } +} + +pub fn evidence_bounty() -> RadrootsEvidenceBounty { + RadrootsEvidenceBounty { + schema: RADROOTS_EVIDENCE_BOUNTY_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + d_tag: "soil-bounty".to_string(), + title: "Soil bounty".to_string(), + summary: None, + topics: vec!["soil".to_string()], + target_refs: vec![event_ref('a', KIND_KNOWLEDGE_CLAIM)], + reward_note: None, + closes_at: None, + } +} + +pub fn knowledge_change_proposal() -> RadrootsKnowledgeChangeProposal { + RadrootsKnowledgeChangeProposal { + schema: RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + target: event_ref('b', KIND_KNOWLEDGE_CLAIM), + proposal_type: "amend".to_string(), + summary: "Clarify scope".to_string(), + rationale: None, + evidence_refs: vec![event_ref('c', KIND_KNOWLEDGE_SOURCE)], + supersedes: Vec::new(), + } +} + +pub fn contribution_attestation() -> RadrootsContributionAttestation { + RadrootsContributionAttestation { + schema: RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + contributor_pubkey: hex_64('a'), + contribution_type: "review".to_string(), + subject_refs: vec![event_ref('d', KIND_KNOWLEDGE_REVIEW)], + summary: "Reviewed synthetic claim".to_string(), + evidence_refs: vec![event_ref('e', KIND_KNOWLEDGE_REVIEW)], + } +} + +pub fn knowledge_valid_fixtures() -> Vec<RadrootsKnowledgeFixtureCase> { + vec![ + RadrootsKnowledgeFixtureCase { + id: "wiki_article_valid", + contract_id: "radroots.wiki.article.v1", + kind: KIND_WIKI_ARTICLE, + data: RadrootsKnowledgeFixture::WikiArticle(wiki_article()), + }, + RadrootsKnowledgeFixtureCase { + id: "wiki_redirect_valid", + contract_id: "radroots.wiki.redirect.v1", + kind: KIND_WIKI_REDIRECT, + data: RadrootsKnowledgeFixture::WikiRedirect(wiki_redirect()), + }, + RadrootsKnowledgeFixtureCase { + id: "wiki_merge_request_valid", + contract_id: "radroots.wiki.merge_request.v1", + kind: KIND_WIKI_MERGE_REQUEST, + data: RadrootsKnowledgeFixture::WikiMergeRequest(wiki_merge_request()), + }, + RadrootsKnowledgeFixtureCase { + id: "knowledge_source_valid", + contract_id: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, + kind: KIND_KNOWLEDGE_SOURCE, + data: RadrootsKnowledgeFixture::KnowledgeSource(knowledge_source()), + }, + RadrootsKnowledgeFixtureCase { + id: "knowledge_claim_valid", + contract_id: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, + kind: KIND_KNOWLEDGE_CLAIM, + data: RadrootsKnowledgeFixture::KnowledgeClaim(knowledge_claim()), + }, + RadrootsKnowledgeFixtureCase { + id: "knowledge_relation_valid", + contract_id: RADROOTS_KNOWLEDGE_RELATION_SCHEMA, + kind: KIND_KNOWLEDGE_RELATION, + data: RadrootsKnowledgeFixture::KnowledgeRelation(knowledge_relation()), + }, + RadrootsKnowledgeFixtureCase { + id: "knowledge_review_valid", + contract_id: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, + kind: KIND_KNOWLEDGE_REVIEW, + data: RadrootsKnowledgeFixture::KnowledgeReview(knowledge_review()), + }, + RadrootsKnowledgeFixtureCase { + id: "knowledge_field_report_valid", + contract_id: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, + kind: KIND_KNOWLEDGE_FIELD_REPORT, + data: RadrootsKnowledgeFixture::KnowledgeFieldReport(knowledge_field_report()), + }, + RadrootsKnowledgeFixtureCase { + id: "evidence_bounty_valid", + contract_id: RADROOTS_EVIDENCE_BOUNTY_SCHEMA, + kind: KIND_EVIDENCE_BOUNTY, + data: RadrootsKnowledgeFixture::EvidenceBounty(evidence_bounty()), + }, + RadrootsKnowledgeFixtureCase { + id: "knowledge_change_proposal_valid", + contract_id: RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA, + kind: KIND_KNOWLEDGE_CHANGE_PROPOSAL, + data: RadrootsKnowledgeFixture::KnowledgeChangeProposal(knowledge_change_proposal()), + }, + RadrootsKnowledgeFixtureCase { + id: "contribution_attestation_valid", + contract_id: RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA, + kind: KIND_CONTRIBUTION_ATTESTATION, + data: RadrootsKnowledgeFixture::ContributionAttestation(contribution_attestation()), + }, + ] +} + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::{ + RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES, RADROOTS_KNOWLEDGE_FIXTURE_NAMESPACE, + RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS, knowledge_valid_fixtures, + }; + + #[test] + fn valid_fixture_catalog_covers_all_contract_ids() { + let fixtures = knowledge_valid_fixtures(); + assert_eq!(fixtures.len(), RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS.len()); + for contract_id in RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS { + assert!( + fixtures + .iter() + .any(|fixture| fixture.contract_id == contract_id), + "{contract_id}" + ); + } + assert_eq!( + RADROOTS_KNOWLEDGE_FIXTURE_NAMESPACE, + "radroots-knowledge-fixture-v1" + ); + } + + #[test] + fn adversarial_fixture_catalog_covers_required_cases() { + let expected = [ + "malformed_tags", + "wrong_schema", + "missing_contract_id", + "private_coordinate_leakage", + "unsupported_contract_shape", + "invalid_nip54_d_tag", + "id_mismatch", + "signature_invalidity", + ]; + assert_eq!( + RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES.len(), + expected.len() + ); + for id in expected { + assert!( + RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES + .iter() + .any(|fixture| fixture.id == id), + "{id}" + ); + } + } +} diff --git a/crates/test_fixtures/src/lib.rs b/crates/test_fixtures/src/lib.rs @@ -1,6 +1,8 @@ #![cfg_attr(coverage_nightly, feature(coverage_attribute))] #![forbid(unsafe_code)] +pub mod knowledge; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ApprovedFixtureIdentity { pub label: &'static str, diff --git a/tools/xtask/Cargo.toml b/tools/xtask/Cargo.toml @@ -10,6 +10,9 @@ publish = false authors = ["Tyson Lupul <tyson@radroots.org>"] [dependencies] +radroots_events_codec = { workspace = true, features = [ + "contract-manifest", +], default-features = false } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha2 = { workspace = true } diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -12,6 +12,12 @@ const ROOT_RELEASE_POLICY_RELATIVE: &str = "foundation/contracts/release_runtime/mounted_rust_crates/publish-policy.toml"; const CONFORMANCE_ROOT_RELATIVE: &str = "contracts/conformance"; const CONFORMANCE_SCHEMA_RELATIVE: &str = "contracts/conformance/schema/vector.schema.json"; +const KNOWLEDGE_MANIFEST_RELATIVE: &str = + "contracts/knowledge/knowledge_event_contract_manifest.v1.json"; +const KNOWLEDGE_MANIFEST_SHA256_RELATIVE: &str = + "contracts/knowledge/knowledge_event_contract_manifest.v1.sha256"; +const KNOWLEDGE_REGENPROTO_COMPATIBILITY_RELATIVE: &str = + "contracts/conformance/vectors/knowledge/regenproto_compatibility.v1.json"; const RELEASE_POLICY_ENV: &str = "RADROOTS_MOUNTED_RUST_CRATE_PUBLISH_POLICY"; const EVENT_BOUNDARY_MATRIX_ENV: &str = "RADROOTS_EVENT_BOUNDARY_MATRIX"; const COVERAGE_REQUIRED_THRESHOLD: f64 = 100.0; @@ -2050,6 +2056,57 @@ fn validate_all_conformance_vectors( Ok(()) } +pub fn write_knowledge_contract_manifest(workspace_root: &Path) -> Result<(), String> { + let manifest_json = radroots_events_codec::contract_manifest_json() + .map_err(|error| format!("serialize knowledge contract manifest: {error}"))?; + let manifest_sha256 = radroots_events_codec::contract_manifest_sha256() + .map_err(|error| format!("hash knowledge contract manifest: {error}"))?; + let manifest_path = workspace_root.join(KNOWLEDGE_MANIFEST_RELATIVE); + let hash_path = workspace_root.join(KNOWLEDGE_MANIFEST_SHA256_RELATIVE); + if let Some(parent) = manifest_path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("create {}: {error}", parent.display()))?; + } + fs::write(&manifest_path, manifest_json) + .map_err(|error| format!("write {}: {error}", manifest_path.display()))?; + fs::write(&hash_path, format!("{manifest_sha256}\n")) + .map_err(|error| format!("write {}: {error}", hash_path.display()))?; + validate_knowledge_contract_manifest(workspace_root) +} + +pub fn validate_knowledge_contract_manifest(workspace_root: &Path) -> Result<(), String> { + let expected_json = radroots_events_codec::contract_manifest_json() + .map_err(|error| format!("serialize knowledge contract manifest: {error}"))?; + let expected_sha256 = radroots_events_codec::contract_manifest_sha256() + .map_err(|error| format!("hash knowledge contract manifest: {error}"))?; + let manifest_path = workspace_root.join(KNOWLEDGE_MANIFEST_RELATIVE); + let hash_path = workspace_root.join(KNOWLEDGE_MANIFEST_SHA256_RELATIVE); + let actual_json = fs::read_to_string(&manifest_path) + .map_err(|error| format!("read {}: {error}", manifest_path.display()))?; + let actual_hash = fs::read_to_string(&hash_path) + .map_err(|error| format!("read {}: {error}", hash_path.display()))?; + + if actual_json != expected_json { + return Err(format!( + "knowledge manifest {} is stale; run cargo xtask contract knowledge-manifest --write", + KNOWLEDGE_MANIFEST_RELATIVE + )); + } + if actual_hash != format!("{expected_sha256}\n") { + return Err(format!( + "knowledge manifest hash {} is stale; run cargo xtask contract knowledge-manifest --write", + KNOWLEDGE_MANIFEST_SHA256_RELATIVE + )); + } + + let bundle = load_contract_bundle(workspace_root)?; + validate_conformance_vector_file( + &workspace_root.join(KNOWLEDGE_REGENPROTO_COMPATIBILITY_RELATIVE), + &bundle.manifest.contract.version, + )?; + Ok(()) +} + #[cfg(not(test))] fn validate_missing_conformance_vectors( _workspace_root: &Path, diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -14,6 +14,7 @@ use std::process::ExitCode; fn usage() { eprintln!("usage:"); eprintln!(" cargo xtask contract validate"); + eprintln!(" cargo xtask contract knowledge-manifest [--write]"); eprintln!(" cargo xtask release preflight"); eprintln!(" cargo xtask coverage run-crate --crate <crate> [--out <dir>]"); eprintln!(" cargo xtask coverage required-crates"); @@ -53,6 +54,7 @@ fn validate_contract() -> Result<(), String> { contract::load_contract_bundle(&root) .and_then(|bundle| contract::validate_contract_bundle(&bundle)) .and_then(|_| contract::validate_canonical_event_boundary(&root)) + .and_then(|_| contract::validate_knowledge_contract_manifest(&root)) } #[cfg_attr(coverage_nightly, coverage(off))] @@ -70,6 +72,13 @@ fn run_release(args: &[String]) -> Result<(), String> { fn run_contract(args: &[String]) -> Result<(), String> { match args.first().map(String::as_str) { Some("validate") => validate_contract(), + Some("knowledge-manifest") => { + if args.get(1).map(String::as_str) == Some("--write") { + contract::write_knowledge_contract_manifest(&workspace_root()) + } else { + contract::validate_knowledge_contract_manifest(&workspace_root()) + } + } _ => Err("unknown contract subcommand".to_string()), } } @@ -235,5 +244,6 @@ mod tests { fn run_contract_dispatches_validate_command() { let _guard = lock_workspace(); run_contract(&["validate".to_string()]).expect("contract validate"); + run_contract(&["knowledge-manifest".to_string()]).expect("contract knowledge manifest"); } }