Tim

Tim's avatar
Tim
timp@iris.to
npub1yn8h...6pzj
Poastor, internet explorer, yolo coder #PardonSamourai
image My dad worked on computers before I was born, back when machines did not pretend to be friendly. He wrote the code, built the machines, wired them together, and listened for sounds that meant trouble. Fans had a pitch and drives had a rhythm. When something went wrong, you often heard it before you saw it. In the eighties he became a system administrator, before that phrase turned into a job title. Back then it meant you owned the whole system. There was no one else to hand it off to. If something failed, it failed right there, in the same room. It left you with little choice other than rolling a cigarette and fixing it. Back then, systems did not reach very far. That was not a limitation. It was the reason they worked. A disk could fail without pulling anything else down with it. A program could crash and stay crashed. You always knew where the edge was because it was right there in the room with you. Trust lived inside those edges. If you gave something access, you knew what it could touch. If you handed someone a key, you knew which door it opened. When a mistake happened, the damage stopped where the system stopped. Abuse existed, but it demanded effort. You had to show up. You had to spend time and hardware and attention. That way of working did not vanish because it was flawed. It vanished because it could not survive success. More people started using computers. Machines began to speak across rooms, then across cities, then across continents. Software left the building and never came back. At some point nobody could point at the whole system anymore. Responsibility stretched thin and then dissolved. Nothing about that was reckless. It was scale doing what scale does. The failures changed shape. Now when an application disappears, your data goes with it. When an account closes, years of context vanish in the same motion. We pretend this is normal. We call it convenience. We accept it because pushing back feels futile. The old alternatives have faded far enough that we barely remember them. These systems assume they will keep working. They assume the platform will still exist tomorrow. They assume nothing important will fail at the wrong time. When those assumptions break, everything breaks together. That is also why abuse spreads so easily. Identity is cheap because it has no weight. Authority slips in unnoticed because it is never clearly granted. Reach appears by default. You do not earn it. You inherit it by showing up. Moderation shows up after the damage is done. There is nowhere else for it to go. By that point the system has already chosen what spreads and what survives. At scale it turns into endless cleanup, not control. We tried to fix it with cryptography. We locked the wires. We stamped messages. We built systems that promised safety if you followed the rules closely enough. What people got were setups that offered no forgiveness. One wrong move and the door slammed shut. Most people stepped away because the price of a small error was total loss. Years could vanish without warning. That is not security. That is fragility. Even Phil Zimmermann, who invented PGP, has said he does not use it himself, because he couldnโ€™t be bothered spending time learning how to set it up correctly. The failure was not the math. It was where we placed it. There is another way to approach this. It starts by letting go of the idea that software should be permanent. It accepts that things will fail. It treats that as normal instead of catastrophic. This is where 2WAY comes in. It starts from a simple idea: On 2WAY, your data does not belong to the application that happens to display it. It lives locally, shaped in a way other software can understand. An app can leave without dragging your history behind it. Another can step in and continue. This changes how access works. Nothing acts just because it exists. Permission has to be given. Not once, not vaguely, and not forever. When something is allowed to do something, that allowance has a shape. When it ends, it ends cleanly. You do not see the cryptography most of the time. It does its work quietly. What you see are moments where a decision is required. A choice to allow something. A choice to stop it later. Recovery is expected and mistakes do not poison the whole system. As a result, failure stops spreading. If a service disappears, your data stays put. If software misbehaves, it cannot wander into places it was never invited to. If trust breaks, it breaks along a line you can point at. Abuse becomes harder because it has to push against structure. New identities arrive without weight. They do not reach far unless other identities lay the groundwork for them. That takes time. It takes effort. It cannot be faked cheaply. Misinformation looks different too. The problem was never that people lie. The problem is that content floats free of its past. In this kind of system, where something came from is not a mystery. What happened to it along the way leaves marks. You are not forced to believe any of it. You can see enough to decide. This matters more as machines begin to act on our behalf. An assistant that can do everything is dangerous. One that can only act within clear limits is dull in the right way. When it fails, it stops instead of spreading the damage. Something else happens along the way. Patterns that rely on quiet overreach stop working. An application cannot slowly take more than it was given. It cannot blur the boundary and hope no one notices. That kind of behavior becomes visible because it has to ask. Power shifts. Platforms lose their grip because they no longer own the ground beneath you. Software competes again on usefulness. Lock-in fades because there is nothing left to hold. None of this fixes people. Conflict remains. Deception remains. But the systems stop amplifying the worst behavior just because it is cheap. We are moving into a world filled with generated voices, synthetic images, and automated decisions. Implicit trust will not survive. Assumptions turn into attack surfaces. Silence becomes risk. The only thing that scales safely is clarity. Clear authority. Explicit boundaries. Systems that know what they are allowed to do and nothing beyond that. Software should be allowed to die. Your data should stay where it is. Your identity should not thin out or vanish because a service closed its doors. That is what 2WAY is trying to make boring again. It is not a product yet. It is a shape. The protocol and backend are written down. The boundaries exist. The hard parts are named. What exists today is the structure, not the machine that runs it. I am still finalizing that structure. I am looking for weak joints. I am checking where assumptions leak. I want the limits to hold before anything is turned on. When that work settles, I will start another build iteration. The protocol and backend specifications are practically ready, MIT licensed, and live here: If this way of thinking aligns with you, the work is open. There is room to work on it together.
Nostr's NIP-01 explains what an event looks like and how the cryptography works. It defines the data model. It does not describe a clear validation model, and I could not find one elsewhere. So I tried to write one. Feedback is very welcome.
Nostr's NIP-01 explains what an event looks like and how the cryptography works. It defines the data model. It does not describe a clear validation model, and I could not find one elsewhere. So I tried to write one. Feedback is very welcome.
# NIP Proposal: Strict Event Validation `draft` `optional` `client` `relay` This NIP defines a strict, uniform validation pipeline for NIP-01 events, so clients and relays can make the same decision about what is valid, what is safe to store, and what is safe to render. It does not change the NIP-01 event format or message flow. ## Motivation NIP-01 defines the event format, how `id` is derived, and how relays acknowledge acceptance or rejection. In practice, different implementations apply different checks, in different orders, and sometimes only partially. This causes common issues: * Clients generate events that some relays reject, because validation differs. * Clients accept and render malformed events that should be rejected, causing crashes, broken threads, and security risks. * Relays accept malformed tags or inconsistent types, which later break indexing, filtering, and downstream clients. * Libraries implement "whatever worked with this relay" instead of "what the protocol says". The goal of this NIP is to standardize a minimal, strict pipeline that is: * deterministic * cheap to run * compatible with NIP-01 semantics * implementable on the client first, without requiring changes from relays * usable by relays as a predictable acceptance layer ## Status and scope This NIP is optional. It specifies validation behavior that may be implemented by clients, relays, or both. This NIP does not: * introduce new event fields * introduce new message types * define new event kinds or tags * define relay policy or moderation rules beyond validation * require changes to signature algorithms or encodings ## Conventions and terminology The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as described in RFC 2119. * **Raw event**: a JSON object received from the network or constructed locally, not yet validated. * **Validated event**: an event that passed all checks in this NIP, and is safe to store, index, and render. * **Strict validation**: the full pipeline defined here. * **Validation context**: local configuration and relay limits, such as maximum sizes or timestamp windows. ## Background, NIP-01 invariants NIP-01 defines the on-wire event fields, the `id` derivation serialization, and signature expectations. Relays respond to `["EVENT", <event>]` with an `["OK", <event_id>, <true|false>, <message>]`, where `<message>` uses a machine-readable prefix followed by `:` and a human-readable message when rejecting, and may be empty when accepting. ## Specification ### 1. Validation pipeline ordering Implementations MUST validate events in the following order, and MUST stop at the first failure. 1. Parse and basic JSON type check 2. Required fields and basic types 3. Canonical constraints (hex lengths, ranges, integer checks) 4. `id` recomputation and equality check 5. Signature verification 6. Tag structure and standard tag grammar 7. Contextual policy limits (optional, but with a standard mechanism) 8. Apply, store, index, render Rationale: the pipeline enforces a single "gate" that all feature code depends on, so rendering, indexing, threading, and filtering never touch untrusted structures. ### 2. Parse and basic shape A raw event MUST be a JSON object. If parsing fails, or the value is not a JSON object, the event is invalid. ### 3. Required fields and basic types A raw event MUST contain the following fields, matching the NIP-01 event format. * `id`, type string * `pubkey`, type string * `created_at`, type number * `kind`, type number * `tags`, type array * `content`, type string * `sig`, type string If any required field is missing, or any type does not match, the event is invalid. ### 4. Canonical constraints To reduce differences between implementations and unsafe edge cases, strict validation applies the following canonical constraints. #### 4.1 Hex fields The following fields MUST be lowercase hex strings of fixed length: * `id` MUST be 64 hex characters (32 bytes), lowercase * `pubkey` MUST be 64 hex characters (32 bytes), lowercase * `sig` MUST be 128 hex characters (64 bytes), lowercase These constraints align with how NIP-01 describes these fields on the wire. If any field fails these constraints, the event is invalid. #### 4.2 Numeric fields * `created_at` MUST be an integer representing unix timestamp seconds. NIP-01 defines it as unix timestamp in seconds, and uses it as a number in the serialization. * `kind` MUST be an integer in the range 0 to 65535 inclusive, as per NIP-01. If `created_at` or `kind` is not an integer, or if `kind` is out of range, the event is invalid. Note: this NIP chooses strict integer enforcement to avoid cross-language float and integer conversion differences. NIP-01 serialization uses numbers, but the semantics described for `created_at` and `kind` are integer-based. Very old events (roughly 2021โ€“2022) occasionally used floating-point `created_at` values; strict implementations may reject those, or make this check configurable if backward compatibility is required. Note: the `kind` range here is the NIP-01 baseline. Some later NIPs use higher kind numbers in practice; strict implementations may choose to accept them or make the allowed range configurable. ### 5. Canonical `id` recomputation Strict validation MUST recompute `id` exactly as described in NIP-01 and compare it for equality with the provided `id`. #### 5.1 Serialization input structure The serialization input MUST be the JSON array: ``` [ 0, <pubkey, as a lowercase hex string>, <created_at, as a number>, <kind, as a number>, <tags, as an array of arrays of non-null strings>, <content, as a string> ] ``` This is NIP-01's specified structure for deriving `event.id`. #### 5.2 Tag value constraint for hashing Because the serialization requires `<tags, as an array of arrays of non-null strings>`, strict validation MUST ensure, before hashing, that: * `tags` is an array * every element of `tags` is an array * every element of each tag array is a string * no element is null If these constraints fail, the event is invalid. #### 5.3 JSON serialization rules Strict validation MUST follow NIP-01 rules to prevent implementation differences: * UTF-8 encoding * no whitespace, line breaks, or unnecessary formatting * the required escape rules for the `content` field as defined in NIP-01 #### 5.4 Hash check Compute: * `payload = utf8(json_serialize(serialization_array))` * `hash = sha256(payload)` * `expected_id = lowercase_hex(hash)` The event is valid only if `expected_id == event.id`. ### 6. Signature verification After `id` matches, strict validation MUST verify the signature: * Verify that `sig` is a valid Schnorr signature over the `id`, using `pubkey`, per NIP-01's stated signature scheme. If signature verification fails, the event is invalid. ### 7. Tag grammar, general rules After cryptographic validity, strict validation enforces uniform tag structure rules. #### 7.1 Basic tag rules * Each tag MUST be an array of one or more strings. * The first element `tag[0]` is the tag name, and MUST be a non-empty string. * All elements of a tag MUST be strings, and MUST NOT be null (this is already required for hashing). If any of these fail, the event is invalid. #### 7.2 Standard tags NIP-01 defines standard tags `e`, `p`, and `a` and their conventional structure. Strict validation enforces the following when these tags appear. ##### `e` tag If `tag[0] == "e"`: * `tag[1]` MUST exist and MUST be a 64-character lowercase hex event id * `tag[2]` MAY exist and is a recommended relay URL string * `tag[3]` MAY exist and MUST be a 64-character lowercase hex pubkey if present ##### `p` tag If `tag[0] == "p"`: * `tag[1]` MUST exist and MUST be a 64-character lowercase hex pubkey * `tag[2]` MAY exist and is a recommended relay URL string ##### `a` tag If `tag[0] == "a"`: * `tag[1]` MUST exist and MUST follow the NIP-01 address format rules for addressable or replaceable events, including the required trailing colon for normal replaceable events as specified. * `tag[2]` MAY exist and is a recommended relay URL string If a standard tag fails its grammar, the event is invalid. Note: This NIP does not attempt to validate the meaning of non-standard tags, only their structural safety. Non-normative note: the exact `a` tag address format is specified in NIP-33 and related NIPs (for example, NIP-16 and NIP-32). Implementations SHOULD refer to those documents for details. Non-normative note: other NIPs define structured tags (for example, delegation in NIP-26 or external identities in NIP-39). Implementations MAY add tag-specific validation for those tags. Strict validation here only mandates the `e`/`p`/`a` grammar plus safe structure for all tags. ### 8. Contextual policy limits Strict validation defines a uniform way to apply local policy limits, without standardizing a single global policy. Implementations MAY apply limits such as: * maximum message size in bytes of the WebSocket JSON frame (UTF-8) * maximum `content` length in Unicode characters (not bytes) * maximum number of tag arrays * acceptable `created_at` skew relative to local clock * minimum proof of work difficulty if required by a relay NIP-11 defines a standard way for relays to communicate limits such as `max_message_length` (bytes of the WebSocket JSON frame), `max_content_length` (Unicode character count, not bytes), `max_event_tags` (count of tag arrays), `min_pow_difficulty` (as defined in NIP-13), and `created_at_lower_limit`/`created_at_upper_limit` (relay policy limits). Clients implementing this NIP SHOULD apply relay-provided limits consistently when publishing to that relay. Timestamp sanity windows are policy limits, not part of cryptographic validity. This avoids breaking NIP-59 use cases where timestamps may be manipulated and relays may apply their own limits. If a contextual limit is violated, the event SHOULD be rejected with a reason that indicates policy, not cryptographic invalidity. ### 9. Applying validated events After an event passes strict validation, implementations SHOULD treat it as immutable: * store it keyed by `id` * if the same `id` is seen again, treat as duplicate and ignore * build indices and views from the stored canonical structure, not from raw inbound JSON Relays already commonly communicate duplicate handling using the `OK` response message format, and NIP-01 gives examples such as `duplicate: already have this event`. ## Relay behavior ### Accepting `EVENT` Relays implementing this NIP SHOULD perform strict validation for every inbound `EVENT`. * If accepted, respond with `["OK", <event_id>, true, <message>]` where `<message>` MAY be empty. * If rejected, respond with `["OK", <event_id>, false, <message>]` where `<message>` MUST begin with a machine-readable prefix, followed by `:` and a human-readable message. Relays that require authentication via NIP-42 MAY use `AUTH` challenges before accepting events. In those cases, `auth-required:` and `restricted:` prefixes may appear in `OK` or `CLOSED` messages to indicate policy or authorization failure, not event-format or cryptographic invalidity. Clients MUST NOT treat these as `invalid:` failures. ### Recommended error prefixes for strict validation This NIP recommends using the existing NIP-01 error pattern and the `invalid:` prefix family only for NIP-01 structural or cryptographic violations. All other rejections, including NIP-42 authentication or authorization failures, MUST use non-`invalid` policy-style prefixes. Use: * `invalid: parse` for non-JSON or non-object * `invalid: missing-field <field>` * `invalid: type <field>` * `invalid: hex <field>` * `invalid: range kind` * `invalid: id-mismatch` * `invalid: sig` * `invalid: tags-structure` * `invalid: tag-e` * `invalid: tag-p` * `invalid: tag-a` Example: * `["OK", "<id>", false, "invalid: id-mismatch"]` * `["OK", "<id>", false, "invalid: tag-e"]` Policy-related failures SHOULD use non-`invalid` prefixes, for example: * `rate-limited: ...` * `restricted: ...` * `auth-required: ...` * `pow: ...` These prefixes and the overall format are aligned with NIP-01 examples. ## Client behavior Clients implementing this NIP SHOULD: 1. Validate before signing and publishing 2. Validate after receiving from relays, before storing or rendering 3. Ensure feature code never consumes raw events Clients SHOULD apply relay limits when publishing, based on NIP-11 relay information, to reduce rejected publishes and inconsistent behavior across relays. ## Compatibility * This NIP is compatible with the NIP-01 on-wire event format and message flow. * This NIP may cause some older or malformed events to be rejected by strict implementations, especially events with non-integer `created_at` or malformed tags. This is intentional for strict mode. Clients that want maximum backwards compatibility MAY support a "base mode" that only enforces NIP-01 minimums, and a "strict mode" that enforces this NIP. ## Security considerations Strict validation reduces risk by: * preventing malformed tag structures from reaching indexing and rendering * preventing signature bypass through type confusion * ensuring `id` and signature checks happen before any feature logic * preventing divergent `id` calculations across implementations by forcing uniform serialization rules This NIP does not solve spam or abuse by itself, but it provides a safer base for rate limiting and policy enforcement. ## Reference algorithm (pseudocode) ``` function validate_strict(event, context): # 1. parse and shape assert is_object(event) # 2. required fields and basic types require_fields(event, ["id","pubkey","created_at","kind","tags","content","sig"]) assert is_string(event.id) assert is_string(event.pubkey) assert is_number(event.created_at) assert is_number(event.kind) assert is_array(event.tags) assert is_string(event.content) assert is_string(event.sig) # 3. canonical constraints assert is_lower_hex(event.id, 64) assert is_lower_hex(event.pubkey, 64) assert is_lower_hex(event.sig, 128) assert is_integer(event.created_at) assert is_integer(event.kind) assert 0 <= event.kind <= 65535 # optional: allow legacy float created_at in compatibility mode # 4. tags hashing constraint for tag in event.tags: assert is_array(tag) assert len(tag) >= 1 for x in tag: assert is_string(x) and x is not null assert tag[0] != "" # 5. recompute id per NIP-01 ser = [0, event.pubkey, event.created_at, event.kind, event.tags, event.content] payload = utf8(json_serialize_no_whitespace_with_nip01_escapes(ser)) expected_id = sha256_hex_lower(payload) assert expected_id == event.id # 6. verify sig per NIP-01 assert schnorr_verify(pubkey=event.pubkey, msg=event.id, sig=event.sig) # 7. standard tag grammar for tag in event.tags: assert tag[0] != "" if tag[0] == "e": assert len(tag) >= 2 assert is_lower_hex(tag[1], 64) if len(tag) >= 4: assert is_lower_hex(tag[3], 64) if tag[0] == "p": assert len(tag) >= 2 assert is_lower_hex(tag[1], 64) if tag[0] == "a": assert len(tag) >= 2 # includes trailing colon for normal replaceable events assert matches_nip01_a_format(tag[1]) # 8. contextual policy limits (optional) if context has limits: assert passes_limits(event, context.limits) return VALIDATED_EVENT(event) ```