This document provides a clear breakdown of implementation phases for NIP-XX Graph Queries.
Goal: Enable the nostr library to correctly "ignore" unknown filter fields per NIP-01, while preserving them for relay-level processing.
filter.F struct with Extra fieldUnmarshal() to skip unknown keysskipJSONValue() helper functiongraph.ExtractFromFilter() functionhandle-req.goGoal: Create bidirectional indexes for event-to-event references (e-tags).
Event-Event Graph (Forward): eeg
eeg|source_event_serial(5)|target_event_serial(5)|kind(2)|direction(1) = 16 bytes
Event-Event Graph (Reverse): gee
gee|target_event_serial(5)|kind(2)|direction(1)|source_event_serial(5) = 16 bytes
EdgeDirectionETagOut = 0 - Event references another event (outbound)EdgeDirectionETagIn = 1 - Event is referenced by another (inbound)pkg/database/indexes/keys.go)pkg/database/indexes/types/letter.go)pkg/database/save-event.go)pkg/database/etag-graph_test.go)Key Bug Fix: Buffer reuse in transaction required copying key bytes before writing second key to prevent overwrite.
Goal: Implement pure index-based graph traversal functions.
File: pkg/database/graph-traversal.go
// Core primitives (no event decoding required)
func (d *D) GetPTagsFromEventSerial(eventSerial *types.Uint40) ([]*types.Uint40, error)
func (d *D) GetETagsFromEventSerial(eventSerial *types.Uint40) ([]*types.Uint40, error)
func (d *D) GetReferencingEvents(targetSerial *types.Uint40, kinds []uint16) ([]*types.Uint40, error)
func (d *D) GetFollowsFromPubkeySerial(pubkeySerial *types.Uint40) ([]*types.Uint40, error)
func (d *D) GetFollowersOfPubkeySerial(pubkeySerial *types.Uint40) ([]*types.Uint40, error)
func (d *D) GetPubkeyHexFromSerial(serial *types.Uint40) (string, error)
func (d *D) GetEventIDFromSerial(serial *types.Uint40) (string, error)
File: pkg/database/graph-result.go
// GraphResult contains depth-organized traversal results
type GraphResult struct {
PubkeysByDepth map[int][]string // depth -> pubkeys first discovered at that depth
EventsByDepth map[int][]string // depth -> events discovered at that depth
FirstSeenPubkey map[string]int // pubkey hex -> depth where first seen
FirstSeenEvent map[string]int // event hex -> depth where first seen
TotalPubkeys int
TotalEvents int
InboundRefs map[uint16]map[string][]string // kind -> target -> []referencing_ids
OutboundRefs map[uint16]map[string][]string // kind -> source -> []referenced_ids
}
func (r *GraphResult) ToDepthArrays() [][]string // For pubkey results
func (r *GraphResult) ToEventDepthArrays() [][]string // For event results
func (r *GraphResult) GetInboundRefsSorted(kind uint16) []RefAggregation
func (r *GraphResult) GetOutboundRefsSorted(kind uint16) []RefAggregation
graph-traversal.gograph-result_test.go and graph-traversal_test.goGoal: Implement the graph query methods (follows, followers, mentions, thread).
File: pkg/database/graph-follows.go
// TraverseFollows performs BFS traversal of the follow graph
// Returns pubkeys grouped by first-discovered depth (no duplicates across depths)
func (d *D) TraverseFollows(seedPubkey []byte, maxDepth int) (*GraphResult, error)
// TraverseFollowers performs BFS traversal to find who follows the seed pubkey
func (d *D) TraverseFollowers(seedPubkey []byte, maxDepth int) (*GraphResult, error)
// Hex convenience wrappers
func (d *D) TraverseFollowsFromHex(seedPubkeyHex string, maxDepth int) (*GraphResult, error)
func (d *D) TraverseFollowersFromHex(seedPubkeyHex string, maxDepth int) (*GraphResult, error)
File: pkg/database/graph-mentions.go
func (d *D) FindMentions(pubkey []byte, kinds []uint16) (*GraphResult, error)
func (d *D) FindMentionsFromHex(pubkeyHex string, kinds []uint16) (*GraphResult, error)
func (d *D) FindMentionsByPubkeys(pubkeySerials []*types.Uint40, kinds []uint16) (*GraphResult, error)
File: pkg/database/graph-thread.go
func (d *D) TraverseThread(seedEventID []byte, maxDepth int, direction string) (*GraphResult, error)
func (d *D) TraverseThreadFromHex(seedEventIDHex string, maxDepth int, direction string) (*GraphResult, error)
func (d *D) GetThreadReplies(eventID []byte, kinds []uint16) (*GraphResult, error)
func (d *D) GetThreadParents(eventID []byte) (*GraphResult, error)
File: pkg/database/graph-refs.go
func (d *D) AddInboundRefsToResult(result *GraphResult, depth int, kinds []uint16) error
func (d *D) AddOutboundRefsToResult(result *GraphResult, depth int, kinds []uint16) error
func (d *D) CollectRefsForPubkeys(pubkeySerials []*types.Uint40, refKinds []uint16, eventKinds []uint16) (*GraphResult, error)
graph-follows_test.goGoal: Wire up the REQ handler to execute graph queries and generate relay-signed response events.
Key Design Decision: All graph query responses are returned as relay-signed events, enabling:
| Kind | Name | Description |
|---|---|---|
| 39000 | Graph Follows | Response for follows/followers queries |
| 39001 | Graph Mentions | Response for mentions queries |
| 39002 | Graph Thread | Response for thread traversal queries |
New files:
pkg/protocol/graph/executor.go - Executes graph queries and generates signed responsespkg/database/graph-adapter.go - Adapts database to graph.GraphDatabase interfaceModified files:
app/server.go - Added graphExecutor fieldapp/main.go - Initialize graph executor on startupapp/handle-req.go - Execute graph queries and return resultsThe response is a relay-signed event with JSON content:
type ResponseContent struct {
PubkeysByDepth [][]string `json:"pubkeys_by_depth,omitempty"`
EventsByDepth [][]string `json:"events_by_depth,omitempty"`
TotalPubkeys int `json:"total_pubkeys,omitempty"`
TotalEvents int `json:"total_events,omitempty"`
}
Example response event:
{
"kind": 39000,
"pubkey": "<relay_identity_pubkey>",
"created_at": 1704067200,
"tags": [
["method", "follows"],
["seed", "<seed_pubkey_hex>"],
["depth", "2"]
],
"content": "{\"pubkeys_by_depth\":[[\"pk1\",\"pk2\"],[\"pk3\",\"pk4\"]],\"total_pubkeys\":4}",
"sig": "<relay_signature>"
}
### Deliverables (Completed)
- [x] Graph executor with query routing (`pkg/protocol/graph/executor.go`)
- [x] Response event generation with relay signature
- [x] GraphDatabase interface and adapter
- [x] Integration in `handle-req.go`
- [x] All tests passing
---
## Phase 5: Migration & Configuration
**Goal**: Enable backfilling and configuration.
### 5.1 E-tag graph backfill migration
**File**: `pkg/database/migrations.go`
```go
func (d *D) MigrateETagGraph() error {
// Iterate all events
// Extract e-tags
// Create eeg/gee edges for targets that exist
}
File: app/config/config.go
Add:
ORLY_GRAPH_QUERIES_ENABLED - enable/disable featureORLY_GRAPH_MAX_DEPTH - maximum traversal depth (default 16)ORLY_GRAPH_RATE_LIMIT - queries per minute per connectionUpdate relay info document to advertise support and limits.
| Phase | Description | Status | Dependencies |
|---|---|---|---|
| 0 | Filter extension parsing | ✅ Complete | None |
| 1 | E-tag graph index | ✅ Complete | Phase 0 |
| 2 | Graph traversal primitives | ✅ Complete | Phase 1 |
| 3 | High-level traversals | ✅ Complete | Phase 2 |
| 4 | Graph query handler | ✅ Complete | Phase 3 |
| 5 | Migration & configuration | Pending | Phase 4 |
Request:
["REQ", "sub", {"_graph": {"method": "follows", "seed": "abc...", "depth": 2}}]
Response: Single signed event with depth arrays
["EVENT", "sub", {
"kind": 39000,
"pubkey": "<relay_pubkey>",
"content": "[[\"depth1_pk1\",\"depth1_pk2\"],[\"depth2_pk3\",\"depth2_pk4\"]]",
"tags": [["d","follows:abc...:2"],["method","follows"],["seed","abc..."],["depth","2"]],
"sig": "..."
}]
["EOSE", "sub"]
Request:
["REQ", "sub", {"_graph": {"method": "follows", "seed": "abc...", "depth": 2}, "kinds": [0]}]
Response: Graph result + events in depth order
["EVENT", "sub", <kind-39000 graph result>]
["EVENT", "sub", <kind-0 for depth-1 pubkey>]
["EVENT", "sub", <kind-0 for depth-1 pubkey>]
["EVENT", "sub", <kind-0 for depth-2 pubkey>]
...
["EOSE", "sub"]
Request:
["REQ", "sub", {"_graph": {"method": "follows", "seed": "abc...", "depth": 1, "inbound_refs": [{"kinds": [7]}]}}]
Response: Graph result + refs sorted by count (descending)
["EVENT", "sub", <kind-39000 with ref summaries>]
["EVENT", "sub", <kind-39001 target with 523 refs>]
["EVENT", "sub", <kind-39001 target with 312 refs>]
["EVENT", "sub", <kind-39001 target with 1 ref>]
["EOSE", "sub"]