social_wot.go raw

   1  //go:build !(js && wasm)
   2  
   3  package acl
   4  
   5  import (
   6  	"sync"
   7  
   8  	"next.orly.dev/pkg/lol/log"
   9  	"next.orly.dev/pkg/database"
  10  	"next.orly.dev/pkg/nostr/encoders/hex"
  11  )
  12  
  13  // WoTDepthMap maintains a mapping from pubkey serial to WoT depth (1-N).
  14  // It is computed via BFS from anchor pubkeys using the ppg/gpp materialized
  15  // indexes and is shared between the social ACL driver and the GC.
  16  //
  17  // Thread-safe: reads are concurrent, writes (Recompute) take an exclusive lock.
  18  type WoTDepthMap struct {
  19  	mu       sync.RWMutex
  20  	depths   map[uint64]int // pubkey serial → WoT depth (1..maxDepth)
  21  	hexIndex map[string]int // pubkey hex → WoT depth (for ACL layer)
  22  	anchors  [][]byte       // anchor pubkeys (32-byte raw)
  23  	maxDepth int
  24  }
  25  
  26  // NewWoTDepthMap creates a new WoT depth map.
  27  // anchors: 32-byte raw pubkeys of relay owners/admins.
  28  // maxDepth: maximum BFS traversal depth (typically 3).
  29  func NewWoTDepthMap(anchors [][]byte, maxDepth int) *WoTDepthMap {
  30  	if maxDepth <= 0 {
  31  		maxDepth = 3
  32  	}
  33  	if maxDepth > 16 {
  34  		maxDepth = 16
  35  	}
  36  	return &WoTDepthMap{
  37  		depths:   make(map[uint64]int),
  38  		hexIndex: make(map[string]int),
  39  		anchors:  anchors,
  40  		maxDepth: maxDepth,
  41  	}
  42  }
  43  
  44  // Recompute runs BFS from each anchor pubkey using the existing
  45  // TraversePubkeyPubkey with direction="out" and repopulates the depth map.
  46  // For multiple anchors, the minimum depth across all anchors is kept.
  47  func (w *WoTDepthMap) Recompute(db *database.D) error {
  48  	newDepths := make(map[uint64]int)
  49  	newHex := make(map[string]int)
  50  
  51  	for _, anchor := range w.anchors {
  52  		result, err := db.TraversePubkeyPubkey(anchor, w.maxDepth, "out")
  53  		if err != nil {
  54  			log.W.F("WoTDepthMap: BFS failed for anchor %s: %v", hex.Enc(anchor), err)
  55  			continue
  56  		}
  57  
  58  		// TraversePubkeyPubkey returns GraphResult with PubkeysByDepth map[int][]string
  59  		for depth, pubkeys := range result.PubkeysByDepth {
  60  			for _, pkHex := range pubkeys {
  61  				// Convert hex to raw bytes then to serial
  62  				pkBytes, err := hex.Dec(pkHex)
  63  				if err != nil || len(pkBytes) != 32 {
  64  					continue
  65  				}
  66  				serial, err := db.GetPubkeySerial(pkBytes)
  67  				if err != nil {
  68  					continue
  69  				}
  70  				serialVal := serial.Get()
  71  
  72  				// Keep minimum depth across all anchors
  73  				if existing, ok := newDepths[serialVal]; !ok || depth < existing {
  74  					newDepths[serialVal] = depth
  75  					newHex[pkHex] = depth
  76  				}
  77  			}
  78  		}
  79  	}
  80  
  81  	// Also add anchors themselves at depth 0 (owners/admins)
  82  	for _, anchor := range w.anchors {
  83  		serial, err := db.GetPubkeySerial(anchor)
  84  		if err != nil {
  85  			continue
  86  		}
  87  		newDepths[serial.Get()] = 0
  88  		newHex[hex.Enc(anchor)] = 0
  89  	}
  90  
  91  	w.mu.Lock()
  92  	w.depths = newDepths
  93  	w.hexIndex = newHex
  94  	w.mu.Unlock()
  95  
  96  	log.I.F("WoTDepthMap: recomputed with %d pubkeys across %d anchors (max depth %d)",
  97  		len(newDepths), len(w.anchors), w.maxDepth)
  98  
  99  	return nil
 100  }
 101  
 102  // GetDepth returns the WoT depth for a pubkey serial.
 103  // Returns 0 if unknown (not in WoT) -- callers must distinguish
 104  // "depth 0 = anchor" from "not found" using the ok return value.
 105  func (w *WoTDepthMap) GetDepth(pubkeySerial uint64) (depth int, ok bool) {
 106  	w.mu.RLock()
 107  	depth, ok = w.depths[pubkeySerial]
 108  	w.mu.RUnlock()
 109  	return
 110  }
 111  
 112  // GetDepthByHex returns the WoT depth for a hex-encoded pubkey.
 113  // Returns -1 if not in WoT.
 114  func (w *WoTDepthMap) GetDepthByHex(pubkeyHex string) int {
 115  	w.mu.RLock()
 116  	depth, ok := w.hexIndex[pubkeyHex]
 117  	w.mu.RUnlock()
 118  	if !ok {
 119  		return -1
 120  	}
 121  	return depth
 122  }
 123  
 124  // Size returns the number of pubkeys in the depth map.
 125  func (w *WoTDepthMap) Size() int {
 126  	w.mu.RLock()
 127  	defer w.mu.RUnlock()
 128  	return len(w.depths)
 129  }
 130  
 131  // GetDepthForGC implements the WoTProvider interface used by the GC.
 132  // Returns the depth bonus tier: 1, 2, 3 for WoT members, 0 for outsiders.
 133  // Anchors (depth 0) are treated as tier 1.
 134  func (w *WoTDepthMap) GetDepthForGC(pubkeySerial uint64) int {
 135  	depth, ok := w.GetDepth(pubkeySerial)
 136  	if !ok {
 137  		return 0 // outsider
 138  	}
 139  	if depth == 0 {
 140  		return 1 // anchor = same as depth 1
 141  	}
 142  	return depth
 143  }
 144