Slice 4 of Block 2. The pure rule was already parameterised; this adds the policy, cold-start and UI around it. - commons_core: WotParams (sigQty/stepMax/sigValidity, Duniter defaults) + WebOfTrust.membersWith; npubToHex helper (NIP-19 decode) for adding roots. - WotSettings (keystore): the active parameters, configurable — a young network loosens them and tightens as it grows, as Ğ1 did. Defaults Duniter. - TrustReferents: the bootstrap 'seeds' membership is measured from — a bundled asset (empty until real founders are curated, no invented keys) unioned with referents the user adds by npub/QR. Honest cold-start. - TrustCubit: computes the full membership verdict against referents+params alongside the personal circle, and exposes a TrustTier (networkMember > inYourCircle > vouched > unknown). Certifications issued with the active validity (they expire and renew, Duniter rule). - UI: chat trust badge by tier; a 'Network of trust' screen (manage roots + advanced params) reached from the profile. i18n en/es/pt/ast. Tests: WotParams/membersWith, npubToHex, TrustReferents, WotSettings, and TrustCubit tiers/membership. Resolves the WoT-parameters decision (open-decisions §B). Trust net stays empty/undetermined until seeded — by design; users bootstrap their own roots.
217 lines
6.7 KiB
Dart
217 lines
6.7 KiB
Dart
import 'package:commons_core/commons_core.dart';
|
|
import 'package:equatable/equatable.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import '../services/social_service.dart';
|
|
import '../services/social_settings.dart';
|
|
|
|
/// Where a peer stands, from strongest to weakest signal.
|
|
enum TrustTier {
|
|
/// Passes the full Duniter membership rule (sigQty certifications from members,
|
|
/// within stepMax of a bootstrap referent).
|
|
networkMember,
|
|
|
|
/// In your personal circle (you vouch, or a friend-of-a-friend does).
|
|
inYourCircle,
|
|
|
|
/// Vouched for by someone, but not (yet) a member nor in your circle.
|
|
vouched,
|
|
|
|
/// No certifications seen.
|
|
unknown,
|
|
}
|
|
|
|
/// Trust standing of one peer: the full Duniter membership verdict, your own
|
|
/// circle, and the raw certifier count — computed from the public certification
|
|
/// graph with the active [WotParams] and bootstrap referents.
|
|
class TrustState extends Equatable {
|
|
const TrustState({
|
|
this.certifierCount = 0,
|
|
this.iVouch = false,
|
|
this.knownToYou = false,
|
|
this.isNetworkMember = false,
|
|
this.networkBootstrapped = false,
|
|
this.loading = true,
|
|
this.busy = false,
|
|
});
|
|
|
|
/// How many people (network-wide) currently 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 (you, or a
|
|
/// friend-of-a-friend, vouch). Spam-resistant and works from day one.
|
|
final bool knownToYou;
|
|
|
|
/// Whether the peer is a full member per the Duniter rule (sigQty + stepMax
|
|
/// from the bootstrap referents).
|
|
final bool isNetworkMember;
|
|
|
|
/// Whether any bootstrap referents exist at all. When false, network
|
|
/// membership can't be determined yet (the trust net isn't seeded) and the UI
|
|
/// should say so instead of implying the peer failed the rule.
|
|
final bool networkBootstrapped;
|
|
|
|
final bool loading;
|
|
final bool busy;
|
|
|
|
/// The strongest applicable signal, for the badge.
|
|
TrustTier get tier {
|
|
if (isNetworkMember) return TrustTier.networkMember;
|
|
if (knownToYou) return TrustTier.inYourCircle;
|
|
if (certifierCount > 0) return TrustTier.vouched;
|
|
return TrustTier.unknown;
|
|
}
|
|
|
|
TrustState copyWith({
|
|
int? certifierCount,
|
|
bool? iVouch,
|
|
bool? knownToYou,
|
|
bool? isNetworkMember,
|
|
bool? networkBootstrapped,
|
|
bool? loading,
|
|
bool? busy,
|
|
}) =>
|
|
TrustState(
|
|
certifierCount: certifierCount ?? this.certifierCount,
|
|
iVouch: iVouch ?? this.iVouch,
|
|
knownToYou: knownToYou ?? this.knownToYou,
|
|
isNetworkMember: isNetworkMember ?? this.isNetworkMember,
|
|
networkBootstrapped: networkBootstrapped ?? this.networkBootstrapped,
|
|
loading: loading ?? this.loading,
|
|
busy: busy ?? this.busy,
|
|
);
|
|
|
|
@override
|
|
List<Object?> get props => [
|
|
certifierCount,
|
|
iVouch,
|
|
knownToYou,
|
|
isNetworkMember,
|
|
networkBootstrapped,
|
|
loading,
|
|
busy,
|
|
];
|
|
}
|
|
|
|
/// Reads and toggles this user's vouch for [peerPubkey] over a [TrustTransport],
|
|
/// and computes the full Duniter membership verdict against the bootstrap
|
|
/// [referents] and active [params].
|
|
class TrustCubit extends Cubit<TrustState> {
|
|
TrustCubit(
|
|
this._transport, {
|
|
required this.peerPubkey,
|
|
required this.selfPubkey,
|
|
this.referents = const {},
|
|
this.params = WotParams.duniter,
|
|
Future<void> Function()? onDispose,
|
|
}) : _onDispose = onDispose,
|
|
super(const TrustState());
|
|
|
|
final TrustTransport? _transport;
|
|
final String peerPubkey;
|
|
final String selfPubkey;
|
|
|
|
/// Bootstrap referents (Duniter seeds) the membership rule trusts by
|
|
/// construction. Empty = the network isn't seeded yet.
|
|
final Set<String> referents;
|
|
|
|
/// Active web-of-trust parameters (sigQty / stepMax / validity).
|
|
final WotParams params;
|
|
final Future<void> Function()? _onDispose;
|
|
|
|
bool get isOnline => _transport != null;
|
|
|
|
/// Personal "circle" rule: one vouch from your side, out to a friend-of-a-
|
|
/// friend. Loose by design — "people near you", separate from formal
|
|
/// membership.
|
|
static const _circleThreshold = 1;
|
|
static const _circleDistance = 2;
|
|
|
|
/// Loads the certification graph and computes: the peer's certifier count,
|
|
/// whether you vouch, whether they're in your circle, and whether they pass
|
|
/// the full Duniter membership rule against the bootstrap referents.
|
|
Future<void> load() async {
|
|
final transport = _transport;
|
|
if (transport == null) {
|
|
emit(state.copyWith(loading: false));
|
|
return;
|
|
}
|
|
emit(state.copyWith(loading: true));
|
|
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,
|
|
);
|
|
// Full membership: only meaningful once the trust net has referents.
|
|
final members = referents.isEmpty
|
|
? const <String>{}
|
|
: wot.membersWith(seeds: referents, params: params);
|
|
emit(state.copyWith(
|
|
certifierCount: certifiers.length,
|
|
iVouch: certifiers.contains(selfPubkey),
|
|
knownToYou: circle.contains(peerPubkey),
|
|
isNetworkMember: members.contains(peerPubkey),
|
|
networkBootstrapped: referents.isNotEmpty,
|
|
loading: false,
|
|
));
|
|
}
|
|
|
|
/// Adds or removes this user's vouch, then reloads. Never vouches for self.
|
|
/// The certification is issued with the active validity so it expires and
|
|
/// must be renewed (Duniter rule).
|
|
Future<void> toggleVouch() async {
|
|
final transport = _transport;
|
|
if (transport == null || peerPubkey == selfPubkey) return;
|
|
emit(state.copyWith(busy: true));
|
|
if (state.iVouch) {
|
|
await transport.revoke(subjectPubkey: peerPubkey);
|
|
} else {
|
|
await transport.certify(
|
|
subjectPubkey: peerPubkey,
|
|
validity: params.sigValidity,
|
|
);
|
|
}
|
|
await load();
|
|
emit(state.copyWith(busy: false));
|
|
}
|
|
|
|
@override
|
|
Future<void> close() async {
|
|
await _onDispose?.call();
|
|
return super.close();
|
|
}
|
|
}
|
|
|
|
/// Opens a [TrustCubit] wired to the social layer for [peerPubkey], or an
|
|
/// offline one.
|
|
Future<TrustCubit> createTrustCubit(
|
|
SocialService social,
|
|
SocialSettings settings, {
|
|
required String peerPubkey,
|
|
}) async {
|
|
final relays = await settings.relayUrls();
|
|
if (relays.isEmpty) {
|
|
return TrustCubit(null,
|
|
peerPubkey: peerPubkey, selfPubkey: social.publicKeyHex);
|
|
}
|
|
try {
|
|
final session = await social.openSession(relays);
|
|
return TrustCubit(
|
|
session.trust,
|
|
peerPubkey: peerPubkey,
|
|
selfPubkey: social.publicKeyHex,
|
|
onDispose: session.close,
|
|
);
|
|
} catch (_) {
|
|
return TrustCubit(null,
|
|
peerPubkey: peerPubkey, selfPubkey: social.publicKeyHex);
|
|
}
|
|
}
|