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
|
|
@ -14,6 +14,7 @@ export 'src/ids/id_gen.dart';
|
|||
export 'src/social/certification.dart';
|
||||
export 'src/social/geohash.dart';
|
||||
export 'src/social/message_transport.dart';
|
||||
export 'src/social/nostr_ids.dart';
|
||||
export 'src/social/nostr/nostr_channel.dart';
|
||||
export 'src/social/nostr/nostr_connection.dart';
|
||||
export 'src/social/nostr/nostr_message_transport.dart';
|
||||
|
|
|
|||
21
packages/commons_core/lib/src/social/nostr_ids.dart
Normal file
21
packages/commons_core/lib/src/social/nostr_ids.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:nostr/nostr.dart';
|
||||
|
||||
/// Decodes a NIP-19 `npub…` into its 64-char hex public key.
|
||||
///
|
||||
/// Accepts an already-hex key unchanged (so callers can take either form), and
|
||||
/// throws [FormatException] on anything that is neither. Used to add web-of-trust
|
||||
/// referents shared as an `npub` (or scanned from a QR).
|
||||
String npubToHex(String input) {
|
||||
final value = input.trim();
|
||||
if (_isHex64(value)) return value.toLowerCase();
|
||||
if (!value.startsWith('npub1')) {
|
||||
throw const FormatException('not an npub or hex public key');
|
||||
}
|
||||
final decoded = Nip19.decode(payload: value);
|
||||
if (decoded.prefix != Nip19Prefix.npub || !_isHex64(decoded.data)) {
|
||||
throw const FormatException('not an npub public key');
|
||||
}
|
||||
return decoded.data.toLowerCase();
|
||||
}
|
||||
|
||||
bool _isHex64(String s) => RegExp(r'^[0-9a-fA-F]{64}$').hasMatch(s);
|
||||
|
|
@ -1,5 +1,44 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'certification.dart';
|
||||
|
||||
/// The Duniter/Ğ1 web-of-trust parameters. Kept as data (not magic numbers) so
|
||||
/// they can be persisted and tuned as the network grows — a young network needs
|
||||
/// looser values than a mature one, exactly as Ğ1 itself started.
|
||||
///
|
||||
/// - [sigQty]: certifications from existing members needed to become a member.
|
||||
/// - [stepMax]: max certification hops from a bootstrap referent (seed).
|
||||
/// - [sigValidity]: how long a certification counts before it must be renewed.
|
||||
class WotParams extends Equatable {
|
||||
const WotParams({
|
||||
required this.sigQty,
|
||||
required this.stepMax,
|
||||
required this.sigValidity,
|
||||
});
|
||||
|
||||
/// The Ğ1 reference values (sigQty 5, stepMax 5, ~1-year validity — Ğ1 uses a
|
||||
/// longer window, but the seed app renews yearly to match `certify`'s default).
|
||||
static const duniter = WotParams(
|
||||
sigQty: 5,
|
||||
stepMax: 5,
|
||||
sigValidity: Duration(days: 365),
|
||||
);
|
||||
|
||||
final int sigQty;
|
||||
final int stepMax;
|
||||
final Duration sigValidity;
|
||||
|
||||
WotParams copyWith({int? sigQty, int? stepMax, Duration? sigValidity}) =>
|
||||
WotParams(
|
||||
sigQty: sigQty ?? this.sigQty,
|
||||
stepMax: stepMax ?? this.stepMax,
|
||||
sigValidity: sigValidity ?? this.sigValidity,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [sigQty, stepMax, sigValidity];
|
||||
}
|
||||
|
||||
/// Pure web-of-trust computation (Duniter/Ğ1 rules), Flutter-free and I/O-free.
|
||||
/// Answers "who counts as a known member" from certification edges, using two
|
||||
/// rules: **N certifications from existing members** (sigQty) and **within
|
||||
|
|
@ -76,6 +115,17 @@ class WebOfTrust {
|
|||
members(seeds: seeds, threshold: threshold, maxDistance: maxDistance)
|
||||
.contains(pubkey);
|
||||
|
||||
/// Members using a [WotParams] bundle (Duniter naming: sigQty/stepMax).
|
||||
Set<String> membersWith({
|
||||
required Set<String> seeds,
|
||||
required WotParams params,
|
||||
}) =>
|
||||
members(
|
||||
seeds: seeds,
|
||||
threshold: params.sigQty,
|
||||
maxDistance: params.stepMax,
|
||||
);
|
||||
|
||||
/// BFS hop-distance from the nearest seed over certification edges.
|
||||
Map<String, int> _distancesFromSeeds(Set<String> seeds) {
|
||||
final dist = <String, int>{for (final s in seeds) s: 0};
|
||||
|
|
|
|||
34
packages/commons_core/test/social/nostr_ids_test.dart
Normal file
34
packages/commons_core/test/social/nostr_ids_test.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('npubToHex', () {
|
||||
// The canonical NIP-19 example pair.
|
||||
const npub =
|
||||
'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6';
|
||||
const hex =
|
||||
'3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d';
|
||||
|
||||
test('decodes a bech32 npub to its hex public key', () {
|
||||
expect(npubToHex(npub), hex);
|
||||
});
|
||||
|
||||
test('passes a 64-char hex key through (lower-cased)', () {
|
||||
expect(npubToHex(hex), hex);
|
||||
expect(npubToHex(hex.toUpperCase()), hex);
|
||||
});
|
||||
|
||||
test('trims surrounding whitespace', () {
|
||||
expect(npubToHex(' $npub '), hex);
|
||||
});
|
||||
|
||||
test('rejects an nsec (secret key), not a public identity', () {
|
||||
expect(() => npubToHex('nsec1' 'garbage'), throwsFormatException);
|
||||
});
|
||||
|
||||
test('rejects nonsense', () {
|
||||
expect(() => npubToHex('not-a-key'), throwsFormatException);
|
||||
expect(() => npubToHex(''), throwsFormatException);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -79,4 +79,28 @@ void main() {
|
|||
final wot = WebOfTrust.fromCertifications([cert('a', 'a')], now: now);
|
||||
expect(wot.certifierCount('a'), 0);
|
||||
});
|
||||
|
||||
group('WotParams', () {
|
||||
test('Duniter defaults are sigQty 5, stepMax 5, 1-year validity', () {
|
||||
const p = WotParams.duniter;
|
||||
expect(p.sigQty, 5);
|
||||
expect(p.stepMax, 5);
|
||||
expect(p.sigValidity, const Duration(days: 365));
|
||||
});
|
||||
|
||||
test('membersWith uses sigQty/stepMax like the raw rule', () {
|
||||
final seeds = {'s1', 's2', 's3'};
|
||||
final certs = [for (final s in seeds) cert(s, 'newcomer')];
|
||||
final wot = WebOfTrust.fromCertifications(certs, now: now);
|
||||
const params = WotParams(
|
||||
sigQty: 3, stepMax: 5, sigValidity: Duration(days: 365));
|
||||
expect(wot.membersWith(seeds: seeds, params: params),
|
||||
contains('newcomer'));
|
||||
// One short of sigQty → not a member.
|
||||
const strict = WotParams(
|
||||
sigQty: 4, stepMax: 5, sigValidity: Duration(days: 365));
|
||||
expect(wot.membersWith(seeds: seeds, params: strict),
|
||||
isNot(contains('newcomer')));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue