Something went wrong. Try again.
An ATProto Powered Reputation Network
Something went wrong. Try again.
5.6 kB · 182 lines
Go
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183package appview
import ( "context" "crypto/sha256" "database/sql" "encoding/hex" "encoding/json" "errors" "slices" "sync"
"provenance/internal/domain" "provenance/internal/trust")
const cacheAttempts = 3
// A reputation query shares bounded neighbor reads across its author-trust cache misses.type trustReader struct { mu sync.Mutex service *Service input trust.Input tag string mode domain.Mode edges []domain.Vouch records map[[2]string]*domain.Vouch revision int64 loaded bool graph *queryGraph budget *trust.Budget rawBudget *trust.Budget}
func (r *trustReader) Score(ctx context.Context, target string) (trust.Result, error) { r.mu.Lock() defer r.mu.Unlock()
if err := ctx.Err(); err != nil { return trust.Result{}, err } if r.service.cacheTTL <= 0 { return trust.Result{}, errors.New("trust cache TTL must be positive") } input := r.input input.Target = target key, err := cacheKey(r.service.algorithm.Describe(), input, r.tag, r.mode, r.service.softMode) if err != nil { return trust.Result{}, err } for attempt := 0; attempt < cacheAttempts; attempt++ { cached, err := r.service.store.CachedTrust(ctx, key, r.service.now()) if err == nil { return cached, nil } if !errors.Is(err, sql.ErrNoRows) { return trust.Result{}, err } if !r.loaded { if _, ok := r.service.algorithm.(trust.NeighborAlgorithm); ok { revision, err := r.service.store.GraphRevision(ctx) if err != nil { return trust.Result{}, err } if r.rawBudget == nil { r.rawBudget = trust.NewBudget(input.Limits) } r.graph = &queryGraph{store: r.service.store, tag: r.tag, mode: r.mode, revision: revision, rawBudget: r.rawBudget, neighbors: map[string][]domain.Vouch{}, records: map[[2]string]*domain.Vouch{}} r.revision, r.records, r.loaded = revision, r.graph.records, true } } if !r.loaded { limit := r.service.limits.Edges + 1 if input.Source == target { limit = 1 } edges, revision, err := r.service.store.TrustGraph(ctx, r.tag, r.mode, limit) if err != nil { return trust.Result{}, err } if len(edges) > r.service.limits.Edges { return trust.Result{}, trust.ErrLimit } r.edges, r.revision, r.loaded = trust.Select(edges, r.tag, r.mode), revision, true r.records = pathRecords(r.edges) } input.Edges = r.edges input.Graph = nil if r.graph != nil { input.Graph = r.graph } if r.budget == nil { r.budget = trust.NewBudget(input.Limits) } input.Budget = r.budget if input.Source == target && r.graph == nil { input.Edges = nil r.loaded = false } result, err := r.service.algorithm.Calculate(ctx, input) if errors.Is(err, errGraphChanged) { r.loaded, r.graph, r.edges = false, nil, nil continue } if err != nil { return trust.Result{}, err } if len(result.Path) > trust.MaxDepth+1 || len(result.PathScores) > trust.MaxDepth+1 { return trust.Result{}, trust.ErrLimit } result = pathComments(result, r.records) dependencies := pathDependencies(result, r.records) saved, err := r.service.store.SaveTrust(ctx, key, result, dependencies, r.revision, r.service.now().Add(r.service.cacheTTL)) if err != nil { return trust.Result{}, err } if saved { return result, nil } r.loaded, r.graph, r.edges = false, nil, nil } return trust.Result{}, trust.ErrLimit}
// Cache keys include all calculation inputs, but never a private participant threshold.func cacheKey(descriptor trust.Descriptor, input trust.Input, tag string, mode domain.Mode, softMode domain.SoftMode) (string, error) { accounts := trust.Accounts{} for name, values := range input.Accounts { values = slices.Clone(values) slices.Sort(values) accounts[name] = slices.Compact(values) } data, err := json.Marshal(struct { Algorithm trust.Descriptor Source, Target, Tag string Mode domain.Mode Limits trust.Limits Accounts trust.Accounts SoftMode domain.SoftMode `json:"soft_mode,omitempty"` MaxDepth int }{descriptor, input.Source, input.Target, tag, mode, input.Limits, accounts, softMode, trust.MaxDepth}) if err != nil { return "", err } digest := sha256.Sum256(data) return hex.EncodeToString(digest[:]), nil}
func pathRecords(edges []domain.Vouch) map[[2]string]*domain.Vouch { // The selected edges remain immutable for the lifetime of this query. records := make(map[[2]string]*domain.Vouch, len(edges)) for index := range edges { edge := &edges[index] records[[2]string{edge.Author, edge.Subject}] = edge } return records}
func pathDependencies(result trust.Result, records map[[2]string]*domain.Vouch) []trust.Dependency { dependencies := make([]trust.Dependency, 0, len(result.PathScores)) for i := 1; i < len(result.PathScores); i++ { pair := [2]string{result.PathScores[i-1].DID, result.PathScores[i].DID} edge, exists := records[pair] if !exists { continue } source, key := edge.Source, edge.RecordKey if source == "" { source = domain.Collection } if !domain.SoftSource(source) { key = domain.RecordKey(edge.Subject, edge.Tag) } dependencies = append(dependencies, trust.Dependency{Author: edge.Author, Source: source, RecordKey: key}) if source == domain.ListBlockCollection { dependencies = append(dependencies, trust.Dependency{Author: edge.ListAuthor, Source: domain.ListCollection, RecordKey: edge.ListKey}, trust.Dependency{Author: edge.ListAuthor, Source: domain.ListItemCollection, RecordKey: edge.ItemKey}) } } return dependencies}