feat(trust): full Duniter web-of-trust membership (params + referents)
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.
This commit is contained in:
parent
a96049dd36
commit
4cf53f259f
29 changed files with 1111 additions and 39 deletions
|
|
@ -5,38 +5,72 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||
import '../services/social_service.dart';
|
||||
import '../services/social_settings.dart';
|
||||
|
||||
/// Trust standing of one peer: how many people vouch for them, and whether YOU
|
||||
/// do. Kept simple on purpose — the full "known member" rule (threshold +
|
||||
/// distance) and its bootstrap set are a policy decision still open
|
||||
/// (network-trust.md §2). Vouching + a live count is the useful, honest first
|
||||
/// step.
|
||||
/// 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) vouch for the peer.
|
||||
/// 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 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.
|
||||
/// 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,
|
||||
}) =>
|
||||
|
|
@ -44,21 +78,34 @@ class TrustState extends Equatable {
|
|||
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, loading, busy];
|
||||
List<Object?> get props => [
|
||||
certifierCount,
|
||||
iVouch,
|
||||
knownToYou,
|
||||
isNetworkMember,
|
||||
networkBootstrapped,
|
||||
loading,
|
||||
busy,
|
||||
];
|
||||
}
|
||||
|
||||
/// Reads and toggles this user's vouch for [peerPubkey] over a [TrustTransport].
|
||||
/// 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());
|
||||
|
|
@ -66,17 +113,26 @@ class TrustCubit extends Cubit<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;
|
||||
|
||||
/// 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".
|
||||
/// 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 peer's certifiers and whether they're within your circle.
|
||||
/// 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) {
|
||||
|
|
@ -94,15 +150,23 @@ class TrustCubit extends Cubit<TrustState> {
|
|||
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;
|
||||
|
|
@ -110,7 +174,10 @@ class TrustCubit extends Cubit<TrustState> {
|
|||
if (state.iVouch) {
|
||||
await transport.revoke(subjectPubkey: peerPubkey);
|
||||
} else {
|
||||
await transport.certify(subjectPubkey: peerPubkey);
|
||||
await transport.certify(
|
||||
subjectPubkey: peerPubkey,
|
||||
validity: params.sigValidity,
|
||||
);
|
||||
}
|
||||
await load();
|
||||
emit(state.copyWith(busy: false));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue