feat(block2): 'in your circle' trust — friend-of-a-friend, not a raw count

A network-wide 'N people vouch' count invites spam and means little. Now the
trust banner computes whether the peer is within YOUR circle — you vouch for
them, or someone you vouch for does (friend-of-a-friend) — via the pure
WebOfTrust rule (seeds={you}, threshold 1, distance 2). When known, the banner
turns green with 'In your circle' (else it falls back to the count).

The full member policy (threshold/distance/bootstrap) stays open (network-trust
§2); this is a deliberately loose 'people near you' heuristic.

Analyzer clean; trust cubit test adds a friend-of-a-friend vs stranger case
(run flutter test locally to confirm — the widget suite is too slow to run here).
This commit is contained in:
vjrj 2026-07-10 12:10:06 +02:00
parent 46b5537a11
commit f3a7ad198d
10 changed files with 96 additions and 16 deletions

View file

@ -14,30 +14,43 @@ class TrustState extends Equatable {
const TrustState({
this.certifierCount = 0,
this.iVouch = false,
this.knownToYou = false,
this.loading = true,
this.busy = false,
});
/// How many people (network-wide) vouch for the peer.
final int certifierCount;
/// Whether this user vouches for the peer.
final bool iVouch;
/// Whether the peer is within the user's own circle of trust — you vouch for
/// them, or someone you vouch for does (friend-of-a-friend). This is the
/// meaningful signal; a raw network count invites spam.
final bool knownToYou;
final bool loading;
final bool busy;
TrustState copyWith({
int? certifierCount,
bool? iVouch,
bool? knownToYou,
bool? loading,
bool? busy,
}) =>
TrustState(
certifierCount: certifierCount ?? this.certifierCount,
iVouch: iVouch ?? this.iVouch,
knownToYou: knownToYou ?? this.knownToYou,
loading: loading ?? this.loading,
busy: busy ?? this.busy,
);
@override
List<Object?> get props => [certifierCount, iVouch, loading, busy];
List<Object?> get props =>
[certifierCount, iVouch, knownToYou, loading, busy];
}
/// Reads and toggles this user's vouch for [peerPubkey] over a [TrustTransport].
@ -57,7 +70,13 @@ class TrustCubit extends Cubit<TrustState> {
bool get isOnline => _transport != null;
/// Loads the current certifiers of the peer.
/// Bootstrap "circle" rule: one vouch from your side, out to a friend-of-a-
/// friend. Deliberately loose (the full member policy threshold/distance
/// is still open, network-trust.md §2); this is just "people near you".
static const _circleThreshold = 1;
static const _circleDistance = 2;
/// Loads the peer's certifiers and whether they're within your circle.
Future<void> load() async {
final transport = _transport;
if (transport == null) {
@ -65,10 +84,20 @@ class TrustCubit extends Cubit<TrustState> {
return;
}
emit(state.copyWith(loading: true));
final certifiers = await transport.certifiersOf(peerPubkey);
final wot = WebOfTrust.fromCertifications(
await transport.allCertifications(),
now: DateTime.now(),
);
final certifiers = wot.certifiersOf(peerPubkey);
final circle = wot.members(
seeds: {selfPubkey},
threshold: _circleThreshold,
maxDistance: _circleDistance,
);
emit(state.copyWith(
certifierCount: certifiers.length,
iVouch: certifiers.contains(selfPubkey),
knownToYou: circle.contains(peerPubkey),
loading: false,
));
}