-
Notifications
You must be signed in to change notification settings - Fork 33
SWIP-066: Graffiti wall pattern #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
zelig
wants to merge
9
commits into
master
Choose a base branch
from
swip-66-graffiti-wall
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
532f438
swip-66: graffiti wall pattern — first draft from dictation round 1
zelig 18f2472
swip-66: round 2 — anchor A=H(T), mode-1 id=H(T), content on writer's…
zelig 9734892
swip-66: round 3 — tag-advertising business line + worked example, co…
zelig 449a964
swip-66: round 4 — H(A|C) restored, ads need no content pointer (exis…
zelig d3bd3e3
swip-66: broker contributors-feed parameters confirmed (owner = broke…
zelig e63f747
swip-66: contributors feed granularity — one update per entry (timest…
zelig aca3cdd
swip-66: split type/category per SWIP-0
zelig 2fef16e
Define graffiti wall concept for public writing
zelig b0a7e12
Revise Mode 2 description for clarity
zelig File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| --- | ||
| SWIP: 66 | ||
| title: Graffiti wall pattern | ||
| author: Viktor Trón (@zelig) | ||
| discussions-to: https://discord.gg/Q6BvSkCv | ||
| status: Draft | ||
| type: Standards Track | ||
| category: Interface | ||
| created: 2026-08-11 | ||
| --- | ||
|
|
||
| <!-- The anti-GSOC. Replaces the shared-owner-key graffiti pattern with own-identity | ||
| writers mining into an anchor neighbourhood. Freestanding pattern; BPS (SWIP-60) enters | ||
| only as one of the write/read modes. --> | ||
|
|
||
| - **Business line**: a permissionless public write-board — a **wall** — named by any | ||
| topic: anyone can write an entry under **their own signature**, everyone can read, | ||
| and every entry has an accountable author. Headline application: **tag-based | ||
| advertising** — announcing a videocast (`A`) on crypto-trading (`B`) on Monday the | ||
| 17th (`C`), the publisher puts an advert on the walls `H(A)`, `H(B)`, `H(C)`, | ||
| `H(A‖B)`, `H(A‖C)`, `H(B‖C)` and `H(A‖B‖C)`, and anyone following any of those tag | ||
| combinations discovers it. | ||
| - **Dev line**: no new wire frames and no proto change; implement (1) writer-side | ||
| mining helpers — ephemeral-owner (MOC, SWIP-42) and span-index — in client | ||
| libraries (bee-js or other JS libs), | ||
| (2) the wall-entry validation rule (span-index construction) wherever SOCs are | ||
| validated for ANCHOR-bound delivery, (3) reader plumbing over the existing | ||
| GSOC-style subscription, `/moc/subscribe`, and BPS ANCHOR cohorts (SWIP-60, | ||
| [#104](https://github.com/ethersphere/SWIPs/pull/104)); done per the conformance | ||
| section. | ||
| - **DISC: NO** — standard SOC validation and the existing implicit-binding rule | ||
| `PO(addr, anchor) >= PO_MIN = 16`; a wrapped chunk with an arbitrary span and empty | ||
| payload is already allowed by storage, so nothing changes below the pattern. | ||
|
|
||
| ## Simple Summary | ||
|
|
||
| A **graffiti wall** is the set of single-owner chunks mined into the neighbourhood of | ||
| an anchor derived from a public topic. The topic — usually a 32-byte reference, such | ||
| as a content address or a tag hash — serves both to form entry ids and to derive the | ||
| **anchor**, exactly as in the original GSOC pattern. What changes is the writer's identity: instead of every writer signing | ||
| with one shared, publicly derivable owner key, each writer signs with **their own | ||
| key** and spends mining effort to land their entry near the anchor — by mining an | ||
| ephemeral owner (the MOC pattern), or by mining the feed **index** itself, carried in | ||
| the span field of the wrapped chunk. Anyone can write; everyone can read; no two | ||
| writers ever share a key — and a BPS broker on the wall's cohort doubles as the | ||
| **aggregate feed indexer** over all writers. | ||
|
|
||
| ## Abstract | ||
|
|
||
| This SWIP specifies a “graffiti wall”: a permissionless public write-board keyed by a topic `T` where writers publish signed entries under their own keys and prove wall membership by mining proximity to the anchor `A = H(T)` (via MOC mining or by mining a self-describing span-index entry). Readers discover writers from wall entries and then follow per-writer content feeds under topic `H(T)`. | ||
|
|
||
| ## Motivation | ||
|
|
||
| The original GSOC pattern hands one secret key to the world, and everything wrong with | ||
| it follows from that. The arguments, enumerated: | ||
|
|
||
| 1. In the Book of Swarm, signature attestation on a single-owner chunk is meant as an | ||
| **integrity check**: because the secret key belongs to a *single* owner, no double | ||
| signing on the same id is a reasonable requirement. A shared graffiti key voids the | ||
| premise — anyone can sign anything on any id, so the attestation attests nothing | ||
| and content at a SOC address loses its uniqueness guarantee. | ||
| 2. **Pull sync.** Reconciling divergent versions at one address needs some digest on | ||
| the chunks — which exists to sync postage stamps, but not for the content: pull | ||
| sync assumes one content per address; GSOC breaks the assumption and forces | ||
| content digests into the sync protocol — cf. the "chunk checksum for divergent | ||
| sync" draft ([#101](https://github.com/ethersphere/SWIPs/pull/101)). | ||
| 3. **Incentives.** The redistribution game had to mix the **wrapped address** into | ||
| the sample to motivate storage of multiple payloads at the same address — a | ||
| storage-incentive complication existing solely to accommodate multi-version SOCs. | ||
| 4. **Stamping.** Currently the stamp for any SOC can be used as the stamp on *any | ||
| version* of the same SOC address — unlimited versions ride a single payment. | ||
| 5. **Retrieval locality.** Retrieval of a multi-version SOC is only possible from | ||
| the closest node; caching is problematic — a cache cannot know whether its copy | ||
| is the version wanted. | ||
| 6. **Retrieval protocol.** A retrieve request has a single response; to exhaust the | ||
| versions at an address one must resort to a wanted (matching) / not-wanted (not | ||
| matching) request pattern — which the protocol does not offer. | ||
| 7. **No censorship resistance.** The pattern does not solve the problem it is often | ||
| assumed to: it is not censorship resistant. | ||
| 8. **Collision-prone even when honest.** The genuine happy path — all writers | ||
| well-intentioned — is already collision prone: cooperating writers race on the | ||
| same id with no arbitration. | ||
|
|
||
| ## Specification | ||
|
|
||
| Notation: `H` = keccak256, `H_BMT` = BMT hash, `‖` = concatenation, `ε` = the empty | ||
| byte string. `PO(x, y)` = proximity order; `PO_MIN = 16` (SWIP-60 protocol constant). | ||
| `SIG_O` = signature by owner key `O`; `ECRecover` recovers the signing address. | ||
|
|
||
| ### The wall: topic and anchor | ||
|
|
||
| A wall is named by its **topic** `T` — usually a 32-byte reference (a content | ||
| address, or the hash of a tag or mnemonic string). Everything else is derived from | ||
| it: | ||
|
|
||
| ``` | ||
| A = H(T) // the anchor — also the topic of the writers' content feeds | ||
| ``` | ||
|
|
||
| An entry is **on the wall** iff it is a valid SOC whose address `addr` satisfies | ||
| `PO(addr, A) >= PO_MIN`. The wall is therefore not a data structure anybody maintains | ||
| — it is a *neighbourhood*, and membership is bought with mining effort. | ||
|
|
||
| ### Writing: three modes | ||
|
|
||
| A writer holding identity key `O` (or minting one) produces a wall entry in one of | ||
| three ways. | ||
|
|
||
| **Mode 1 — mined ephemeral owner (the MOC pattern, SWIP-42, | ||
| [#80](https://github.com/ethersphere/SWIPs/pull/80)).** The writer mines an ephemeral | ||
| keypair until the SOC address lands on the wall: | ||
|
|
||
| ``` | ||
| id = H(T) // note: id = A | ||
| mine O_e until PO( H(id ‖ O_e), A ) >= PO_MIN | ||
| ``` | ||
|
|
||
| Content is unconstrained; the entry is a MOC and all MOC tooling applies. The | ||
| ephemeral owner is still *an* owner — unique to this writer, never shared — but the | ||
| entry is not linked to the writer's persistent identity unless the payload links it. | ||
|
|
||
| **Mode 2 — content feed, bare index, brokered (no mining).** When writers connect in | ||
| publisher role to a broker (BPS, SWIP-60), they can publish updates on their **content | ||
| feed** under topic `H(T)` (i.e. `A`), and frames carry the **bare 8-byte sequential | ||
| index in the 32-byte id field** — the carriage rule of SWIP-65 | ||
| ([#106](https://github.com/ethersphere/SWIPs/pull/106)). | ||
|
|
||
| This mode does **not** create a wall entry by itself (it does not buy wall membership | ||
| via `PO(addr, A) >= PO_MIN`); discoverability on the wall still requires a mode-1 or | ||
| mode-3 anchored wall entry that allows readers to recover the writer and follow the | ||
| content feed. Every frame carries the full SOC, so the writer's own key signs as in any | ||
| feed. | ||
|
|
||
| **Mode 3 — normal feed, mined index (the span-index construction).** The writer keeps | ||
| their real identity `O` and mines the feed **index** so that the entry lands on the | ||
| wall. The index is a uint64 and is written into the **span field of the wrapped | ||
| chunk**; the payload is left **empty** for quick filtering. The entry is byte-for-byte | ||
| a feed update on topic `T` at index `SPAN`: | ||
|
|
||
| ``` | ||
| mine SPAN (uint64) until PO( addr, A ) >= PO_MIN where | ||
|
|
||
| id = H( T ‖ SPAN ) | ||
| sig = SIG_O( H( id ‖ H_BMT( SPAN ‖ ε ) ) ) | ||
| addr = H( id ‖ O ) | ||
| c = id ‖ sig ‖ SPAN ‖ ε | ||
| ``` | ||
|
|
||
| Because the index sits in the span field, the entry is **self-describing**: a reader | ||
| holding only the chunk (and knowing `T`) validates it with no external information — | ||
|
|
||
| ``` | ||
| ID = H( T ‖ SPAN ) // recompute id from topic and span | ||
| O = ECRecover( SIG, H( ID ‖ H_BMT(SPAN ‖ ε) ) ) | ||
| addr = H( ID ‖ O ) | ||
| valid iff PO( addr, A ) >= PO_MIN | ||
| ``` | ||
|
|
||
| The mined SPAN thus places a member of the writer's **MIC** in the proximity of the | ||
| anchor: the entry simultaneously belongs to the wall (by address) and to the writer's | ||
| own SOC universe (by owner). The mode-3 entry itself carries no message — it | ||
| *announces* a writer: the content lives on the **owner's content feed for the wall**, | ||
| whose topic is `H(T)` — one hash deeper than the wall's own id preimage, so a content | ||
| update `id = H( H(T) ‖ IDX )` can never clash with a wall entry `id = H( T ‖ SPAN )` | ||
| even when the mined SPAN happens to be a low integer. Note `H(T) = A`: the anchor | ||
| doubles as the content-feed topic. A reader recovers `O` from the entry and follows | ||
| the feed `(H(T), O)` — from index 0, or as a subscription — for the actual graffiti. | ||
| When the entry's existence is itself the message (see the worked example), no content | ||
| feed need exist. | ||
|
|
||
| ### Reading the wall | ||
|
|
||
| - **Full node in the anchor neighbourhood**: wall entries arrive with the reserve; | ||
| filtering **zero-length payloads with a conforming address** yields the mode-3 | ||
| entries directly, and the GSOC-style subscription surfaces entries as they land. | ||
| - **Light client, live**: a BPS cohort with ANCHOR binding and implicit publishers | ||
| (SWIP-60) — the broker's implicit-publisher validation rule | ||
| `PO(addr, anchor) >= PO_MIN` is exactly wall membership, so brokers need no new | ||
| rules; mode-3 entries additionally validate by the span-index chain above. | ||
| - **After the fact**: BPS mediated — see the aggregation section below. Individual | ||
| entries remain ordinary stored chunks, retrievable by address. | ||
|
|
||
| ### Aggregation: the broker as wall indexer | ||
|
|
||
| The one piece that completes the pattern: a BPS broker serving the wall's cohort acts | ||
| as the **aggregate feed indexer** for the wall. The broker sees every entry as it is | ||
| published; each entry names a writer `O`, and each writer's content is a feed | ||
| `(H(T), O)`. The broker aggregates by **publishing its own feed of contributors for | ||
| the topic** — owner the broker's key, topic `H(T)`, **one update per wall entry | ||
| observed**, the granularity most compatible with the self-indexed pot's default | ||
| timestamp key (a broker MAY instead dedup to one update per contributor) — and | ||
| making it **self-indexing** per SWIP-65 | ||
| ([#106](https://github.com/ethersphere/SWIPs/pull/106)). A late joiner then needs | ||
| only the broker feed's latest update: the whirl-only pot in its payload indexes every | ||
| contributor in arrival order, gives O(log n) seek, and authenticates the whole | ||
| history against the broker's one signature. After-the-fact reading of a wall is thus | ||
| a single feed lookup, and trust in the broker is bounded: the broker can withhold a | ||
| contributor from its index, but cannot forge one — every indexed element is a wall | ||
| entry carrying its writer's own signature. | ||
|
|
||
| ### Worked example: tag-based advertising | ||
|
|
||
| A publisher announces a videocast (`A`) on crypto-trading (`B`) on Monday the 17th | ||
| (`C`). Each tag and tag combination is a wall: | ||
|
|
||
| ``` | ||
| T ∈ { H(A), H(B), H(C), H(A‖B), H(A‖C), H(B‖C), H(A‖B‖C) } | ||
| ``` | ||
|
|
||
| For each wall the publisher writes one entry — mode 3, say: mine `SPAN` so that | ||
| `H( H(T ‖ SPAN) ‖ O )` lands within `PO_MIN` of `H(T)`. **No content feed is | ||
| needed**: this is an ad type where the entry's *existence* gives the information — | ||
| the wall names the tags, the recovered owner names the advertiser, and anything more | ||
| is discoverable through the writer's MIC and feeds. A reader interested in | ||
| crypto-trading follows the wall `H(B)`; one interested specifically in Monday's | ||
| videocasts follows `H(A‖C)`. Live discovery is the ANCHOR cohort on each wall; after | ||
| the fact, one lookup of a broker's contributor feed per wall. | ||
|
|
||
| ### Conformance | ||
|
|
||
| 1. A **wall entry** is valid iff it is a valid SOC and `PO(addr, A) >= PO_MIN`, with | ||
| `A = H(T)` derived from the wall topic as specified above. | ||
| 2. A **mode-3 entry** is valid iff, additionally, `id = H(T ‖ SPAN)` for the SPAN | ||
| carried in its wrapped chunk's span field and its payload is empty. | ||
| 3. Readers and brokers MUST ignore chunks in the anchor neighbourhood that fail the | ||
| applicable validity chain; ignoring is silent. | ||
| 4. A broker serving an ANCHOR-bound cohort MUST apply the implicit-publisher PO rule | ||
| of SWIP-60 unchanged; this SWIP adds no broker-side rules. | ||
| 5. Storage nodes MUST NOT reject a wrapped chunk on account of an arbitrary span | ||
| value with an empty payload (already the case — codified here so the pattern's | ||
| floor cannot regress). | ||
|
|
||
| ## Copyright | ||
|
|
||
| Copyright and related rights waived via | ||
| [CC0](https://creativecommons.org/publicdomain/zero/1.0/). | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.