@steve02081504/fount-p2p 0.0.31 → 0.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +79 -0
- package/docs/evfs.md +32 -0
- package/docs/infra.md +25 -0
- package/docs/mesh.md +67 -0
- package/docs/reputation.md +22 -0
- package/docs/runtime.md +62 -0
- package/docs/signaling.md +23 -0
- package/docs/transports.md +92 -0
- package/docs/wire.md +21 -0
- package/package.json +2 -9
- package/sim/AGENTS.md +29 -0
- package/sim/admission.mjs +56 -0
- package/sim/anomaly.mjs +39 -0
- package/sim/apply.mjs +77 -0
- package/sim/attack_space.mjs +113 -0
- package/sim/attacks.mjs +614 -0
- package/sim/behavior.mjs +78 -0
- package/sim/cli.mjs +282 -0
- package/sim/coevolution.mjs +245 -0
- package/sim/cold_start.md +13 -0
- package/sim/constants.mjs +44 -0
- package/sim/discovery.mjs +259 -0
- package/sim/duration.mjs +64 -0
- package/sim/graph_adj.mjs +28 -0
- package/sim/integrity.mjs +58 -0
- package/sim/metrics.mjs +423 -0
- package/sim/model.mjs +1176 -0
- package/sim/optimizer.mjs +250 -0
- package/sim/propagation.mjs +57 -0
- package/sim/report.mjs +176 -0
- package/sim/reputation_social.tunables.json +6 -0
- package/sim/rng.mjs +62 -0
- package/sim/scenarios.mjs +345 -0
- package/sim/sim_pool.mjs +169 -0
- package/sim/sim_worker.mjs +31 -0
- package/sim/social_reputation.mjs +30 -0
- package/sim/space.mjs +312 -0
- package/sim/test/admission.test.mjs +25 -0
- package/sim/test/attack_space.test.mjs +39 -0
- package/sim/test/behavior_vector.test.mjs +37 -0
- package/sim/test/coevolution.test.mjs +32 -0
- package/sim/test/cold_start.test.mjs +46 -0
- package/sim/test/defense.test.mjs +320 -0
- package/sim/test/discovery.test.mjs +68 -0
- package/sim/test/duration.test.mjs +20 -0
- package/sim/test/fidelity.test.mjs +151 -0
- package/sim/test/integrity.test.mjs +56 -0
- package/sim/test/optimizer.test.mjs +24 -0
- package/sim/test/propagation.test.mjs +29 -0
- package/sim/test/sim_model_regressions.test.mjs +110 -0
- package/sim/test/slow_drip.test.mjs +28 -0
- package/sim/test/smoke.test.mjs +52 -0
- package/sim/test/transport.test.mjs +55 -0
- package/sim/test/tunables_apply.test.mjs +64 -0
- package/sim/test/tunables_param_space.test.mjs +74 -0
- package/sim/test/vulnerability.test.mjs +127 -0
- package/sim/transport.mjs +135 -0
- package/sim/tunables_bundle.mjs +71 -0
- package/sim/vulnerability.mjs +532 -0
- package/deno.json +0 -7
package/AGENTS.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# P2P / Federation / Entity Files Guide
|
|
2
|
+
|
|
3
|
+
## Package layers (`@steve02081504/fount-p2p`)
|
|
4
|
+
|
|
5
|
+
| Layer | Directory | Role |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| L0 | `core/` | IDs, logical entity, canonical JSON, bytes |
|
|
8
|
+
| L1 | `crypto/`, `wire/`, `schemas/` | Crypto, wire ingress, canonical validation |
|
|
9
|
+
| L2 | `node/` | `initNode`, identity, entity store, denylist, reputation, storage plugins |
|
|
10
|
+
| L3 | `discovery/`, `link/`, `transport/` | fount network (registry/rooms); `registerLinkProvider` via `./link` or facade |
|
|
11
|
+
| L4 | `trust_graph/`, `mailbox/`, `dag/`, `federation/`, `files/`, `governance/`, `reputation/` | Federation, store-and-forward, DAG, EVFS, tunables |
|
|
12
|
+
|
|
13
|
+
**Outside the package** (shell / frontend; p2p must not import): chat/social semantics, mention rendering, entity identity provisioning, etc. Standalone clients: `import { startNode } from '@steve02081504/fount-p2p'`.
|
|
14
|
+
|
|
15
|
+
**Facade:** `index.mjs`; subpath exports mirror directories.
|
|
16
|
+
|
|
17
|
+
Detail docs: [transports](docs/transports.md) · [mesh](docs/mesh.md) · [signaling](docs/signaling.md) · [runtime](docs/runtime.md) · [infra](docs/infra.md) · [wire](docs/wire.md) · [evfs](docs/evfs.md) · [reputation](docs/reputation.md)
|
|
18
|
+
|
|
19
|
+
### Runtime: isomorphic vs Node
|
|
20
|
+
|
|
21
|
+
| Surface | Runtime | Notes |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `core/*` | Node + browser | No `node:*` builtins |
|
|
24
|
+
| `crypto/crypto.mjs` | Node + browser | `@noble/hashes` + `@noble/curves` only — never `node:crypto` |
|
|
25
|
+
| `crypto/key.mjs` / `crypto/channel.mjs`, disk I/O, LAN/BT, `ws`, CLI / `startNode` | Node (+ Deno bridge) | Do not load the whole package via esm.sh in the browser |
|
|
26
|
+
|
|
27
|
+
Deno / native / BT: [runtime.md](docs/runtime.md).
|
|
28
|
+
|
|
29
|
+
## Conventions
|
|
30
|
+
|
|
31
|
+
- Prefer shared helpers under `utils/`, `core/`, `wire/subscribe`, `wire/adapter` — do not reimplement LRU/TTL/inflight/atomic-fs/shuffle.
|
|
32
|
+
- **No pure-forward aliases:** do not add `fooText`/`fooAlias` that only `return foo(sameArgs)` when the callee already accepts those types (e.g. never wrap `sha256Hex` as `sha256TextHex`). Domain names must add logic or type narrowing, not just rename.
|
|
33
|
+
- **Heterogeneous backends:** normalize at the load boundary (e.g. `link/rtc/ice_local_hostname.mjs` wraps W3C RTC backends); call sites speak one contract.
|
|
34
|
+
- **File naming:** parent directory is scope — short child names. Tunables default `<dir>/tunables.json` (exception: `schemas/part_query.tunables.json`). Subpath `package.json` exports mirror filenames.
|
|
35
|
+
- **Import boundary:** `test/integration/p2p_shell_import_guard.test.mjs`.
|
|
36
|
+
- **No scattered `trim` / `toLowerCase`:** hex IDs must already be lowercase; `normalizeHex64` strips an optional `0x` only — mixed case / whitespace is rejected. Exceptions: JSONL blank lines, SDP fingerprint, CLI/`scripts` parsing.
|
|
37
|
+
- **No `String(x)` / `x || ''` on typed `string`:** if `@param {string}`, use it directly; `String(...)` / `|| ''` / `?? ''` only at optional / `unknown` / disk / inbound boundaries, or number→string.
|
|
38
|
+
- **Optional methods:** `if (fn) return await fn(...)` / `if (fn) …` — never `typeof x === 'function'`.
|
|
39
|
+
|
|
40
|
+
## Tests / tools
|
|
41
|
+
|
|
42
|
+
- `npm test` — pure + integration (Node; `--test-force-exit`)
|
|
43
|
+
- `npm run test:live` — live link / LAN smoke
|
|
44
|
+
- `npm run test:fount` — cross-repo Deno bridge (`test/fount/`); see [runtime.md](docs/runtime.md)
|
|
45
|
+
- `npm run test:sim` — tunables co-evolution (dev-only; [sim/AGENTS.md](sim/AGENTS.md))
|
|
46
|
+
- `node scripts/check-imports.mjs` — relative import check
|
|
47
|
+
- `node scripts/find-unused-exports.mjs` — dead-export scan (`--fount <path>` optional)
|
|
48
|
+
- Assertions: `test/helpers/assert.mjs`
|
|
49
|
+
- Fixed-seed identity: `test/helpers/identity.mjs`
|
|
50
|
+
- Mock discovery: `test/helpers/mock_discovery.mjs`
|
|
51
|
+
|
|
52
|
+
## Hard rules
|
|
53
|
+
|
|
54
|
+
### Trust / ingress
|
|
55
|
+
|
|
56
|
+
- **Untrusted ingress only:** discovery adverts/signals, link/overlay envelopes, group federation frames, `remoteIngest`, `part_timeline_*` / `part_invoke`, `part_query_*`, public manifest (`fed_manifest_data`). Validate / `canonicalize*` / `verifySignedPublicManifest` **only** here.
|
|
57
|
+
- **Trusted after disk:** from `events.jsonl`, only `stripDagEventLocalExtensions` — no re-canonicalization upstream.
|
|
58
|
+
- **Fanout vs targeted / timed collect:** [wire.md](docs/wire.md).
|
|
59
|
+
- **Channel encryption:** per-channel `K_ch`, scheme `channel-key` (`CHANNEL_KEY_SCHEME`); decrypted payloads are untrusted outside DAG Ed25519 context.
|
|
60
|
+
- **Denylist vs personal lists:** node `denylist.json` vs per-entity `personal_block.json` / `personal_hide.json`.
|
|
61
|
+
- **Manifest ACL / transfer owner:** shells register matchers; core does not hard-code chat/social types.
|
|
62
|
+
|
|
63
|
+
### Node / network
|
|
64
|
+
|
|
65
|
+
- **Node data:** `initNode({ nodeDir, entityStore? })` — `node.json`, `network.json`, `denylist.json`, `reputation.json`, `mailbox/`, `chunks/`. Default EntityStore: `{nodeDir}/entities/`. No `FOUNT_*` env knobs — subprocess IPC uses argv.
|
|
66
|
+
- **fount network:** shells use `startNode` / `ensureLinkToNode` / `sendToNodeLink` / rooms — never import `link/` internals or pick a transport. Providers: `registerLinkProvider` from `./link` or facade.
|
|
67
|
+
- **Link `level` vs discovery `priority`:** descending `level` picks data transport (`nostr` = −∞ last resort); ascending `priority` orders handshake/presence media only. [transports.md](docs/transports.md)
|
|
68
|
+
- **Mesh first / no versioning:** ≥N links (K acquaintances + N−K explore); discovery API is `listVisibleNodeHashes` + `connectToNode` only; no topic on the fount-network surface; no version/compat fields. [mesh.md](docs/mesh.md)
|
|
69
|
+
- **Room / registry:** `configureLinkRegistry(opts)` before first `getLinkRegistry`. `startNode` does not take registry options. `createGroupLinkSet` is the kernel; `createScopedLinkRoom` is a dial-all preset. Rooms call `registry.ensureRuntime()` before subscribe/advertise — [runtime.md](docs/runtime.md)
|
|
70
|
+
- **Fetch ≠ apply:** `ingestEncryptedAdvert` vs `noteAdvertPeerHints`; reputation pull never writes — [reputation.md](docs/reputation.md). Public-manifest cache/fanout: [evfs.md](docs/evfs.md). Node-scope attaches: [infra.md](docs/infra.md).
|
|
71
|
+
|
|
72
|
+
## Tunables JSON
|
|
73
|
+
|
|
74
|
+
| File | Directory |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `tunables.json` | `reputation/`, `trust_graph/`, `mailbox/`, `governance/`, `dag/` |
|
|
77
|
+
| `part_query.tunables.json` | `schemas/` |
|
|
78
|
+
|
|
79
|
+
Sim harness: `sim/tunables_bundle.mjs` (dev-only). See [sim/AGENTS.md](sim/AGENTS.md).
|
package/docs/evfs.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# EVFS public manifests & fetch
|
|
2
|
+
|
|
3
|
+
Day-to-day package rules: [AGENTS.md](../AGENTS.md). Implementation: `files/` (`evfs`, `evfs_ref`, `chunk/*`, `manifest/*`).
|
|
4
|
+
|
|
5
|
+
## Storage
|
|
6
|
+
|
|
7
|
+
| What | Path |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Ciphertext chunks (CAS) | `{nodeDir}/chunks/` |
|
|
10
|
+
| Manifests | `{EntityStoreRoot}/{entityHash}/files/{path}.manifest.json` |
|
|
11
|
+
|
|
12
|
+
## Public publish / verify
|
|
13
|
+
|
|
14
|
+
- `publishPublicFile` signs with the recovery key.
|
|
15
|
+
- Remote path: `fed_manifest_get` → `verifySignedPublicManifest` → cache.
|
|
16
|
+
- Signature covers **content fields only**. After verify, drop incoming `meta` except `publicSig`.
|
|
17
|
+
- Profile / avatar meaning belongs in the shell — not in this package.
|
|
18
|
+
|
|
19
|
+
## `fetchPublicManifest`
|
|
20
|
+
|
|
21
|
+
| Case | Behavior |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| Default options | No cache write |
|
|
24
|
+
| Local hit with `publicSig` | Return immediately; fanout revalidates in the background. With `cache: true`, write only when remote `publishedAt` is newer |
|
|
25
|
+
| Cold miss | Await fanout |
|
|
26
|
+
| Same key in flight | Deduped via `utils/inflight_table` |
|
|
27
|
+
|
|
28
|
+
Outer caller timeouts must **not** abort the in-flight work — background fill continues after the caller gives up.
|
|
29
|
+
|
|
30
|
+
## ACL
|
|
31
|
+
|
|
32
|
+
Shells register matchers via the ACL registry. Core does not hard-code chat/social entity types.
|
package/docs/infra.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Infra relay & node-scope attaches
|
|
2
|
+
|
|
3
|
+
Optional public-good overlay + mailbox. Day-to-day rules: [AGENTS.md](../AGENTS.md). Wire part attaches: [wire.md](wire.md).
|
|
4
|
+
|
|
5
|
+
## `startInfra` / `stopInfra`
|
|
6
|
+
|
|
7
|
+
Call after `initNode`. CLI: `npx @steve02081504/fount-p2p`.
|
|
8
|
+
|
|
9
|
+
- **Connectivity debug:** CLI default on; `--quiet` off. Non-CLI: `setConnectivityDebug(true)`.
|
|
10
|
+
- **Priority:** `setInfraPriority({ useLocalReputation })` reads local `reputation.json` only. `stopInfra` resets priority config so the next start does not inherit a ghost weight.
|
|
11
|
+
- **Reputation pull/export is separate:** `pullReputationFromNode` → JSON; `setReputationTable` to apply. Infra does **not** attach `rep_sync`. Donor must `attachReputationSyncWire()` (+ export allowlist); pull side auto-attaches.
|
|
12
|
+
- **`lockReputationMax` / `unlockReputationMax`:** unlock restores the pre-lock score.
|
|
13
|
+
- **`stopInfra` scope:** releases only its own attach refs, restores `maxActive`, clears rate/debug/priority weight.
|
|
14
|
+
|
|
15
|
+
## Node-scope attaches
|
|
16
|
+
|
|
17
|
+
Composable attaches under `transport/node_scope/` — import concrete modules, no directory barrel:
|
|
18
|
+
|
|
19
|
+
- `transport/node_scope/wire` — `ensureNodeScope`, `registerNodeScopeWireHook` (shell custom actions when wire is ready), helpers
|
|
20
|
+
- `transport/node_scope/features` — `attachNodeScopeMailbox` / `Part` / `PartQuery` / `Chunks`, `attachNodeScopeDefaultFeatures`, `attachNodeScopeFeature`
|
|
21
|
+
|
|
22
|
+
Each attach returns `dispose` and is **refcount-shared** (infra + default features can both hold mailbox without double handlers).
|
|
23
|
+
|
|
24
|
+
- `ensureUserRoom` is **slot + runtime only** (default `attachDefaultWires: false`).
|
|
25
|
+
- Full preset: `attachNodeScopeDefaultFeatures()` or `ensureUserRoom({ attachDefaultWires: true })`.
|
package/docs/mesh.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Mesh policy
|
|
2
|
+
|
|
3
|
+
A fount node's **first job is to join and stay on the fount network**. Link presence is not business trust: discovery and dialing stay aggressive; trust and fanout are constrained by reputation / TrustGraph / denylist.
|
|
4
|
+
|
|
5
|
+
Do **not** treat "unrelated ⇒ no interconnect" or "no common group ⇒ no dial" as design premises. Nodes with no acquaintances, no shared groups, and no shell intro must still reach the network via discovery media.
|
|
6
|
+
|
|
7
|
+
No versioning — see [transports.md](transports.md).
|
|
8
|
+
|
|
9
|
+
## Discovery: no topic on the fount-network surface
|
|
10
|
+
|
|
11
|
+
Each discovery / scannable medium exposes only:
|
|
12
|
+
|
|
13
|
+
1. `listVisibleNodeHashes({ limit? })` → `nodeHash[]` visible on that medium
|
|
14
|
+
2. `connectToNode(nodeHash)` → dial via registry (`ensureLinkToNode`)
|
|
15
|
+
|
|
16
|
+
Scans with `roomSecret` return **only that group's visible pool**. Media without group semantics (LAN / BT) return `[]` — do not pour network/LAN nodes into group membership.
|
|
17
|
+
|
|
18
|
+
Topic, relay tags, rendezvous keys, and signal crypto live **only** under `discovery/`. Group flows pass `roomSecret` and other business keys; transport only sees `nodeHash` / opaque bytes.
|
|
19
|
+
|
|
20
|
+
Visible pools (Nostr / BT / LAN) only accept **signature-verified** adverts; forged `body.nodeHash` after decrypt alone does not enter the pool. BT scan also records `peripheralId` via `noteAdvertPeerHints` so `connectToNode` / `ble_gatt` can dial.
|
|
21
|
+
|
|
22
|
+
Group presence / advert watch / signaling fan-in **all** providers that implement the methods — do not hard-bind `nostr` id.
|
|
23
|
+
|
|
24
|
+
No discovery-layer mDNS. LAN UDP presence must carry signed+encrypted network `advertBytes`; unsigned `nodeHash` beacons are ignored. WebRTC ICE `.local` host-candidate filtering is `iceLocalHostnamePolicy` (unrelated to discovery).
|
|
25
|
+
|
|
26
|
+
| Medium | Scan | Dial |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| Nostr | Internal rendezvous; surface is hash list only | Internal signal + trigger link |
|
|
29
|
+
| lan_tcp | Segment presence/beacon (not topic) | TCP dial |
|
|
30
|
+
| BT | Near-field scan | GATT / near-field assist |
|
|
31
|
+
|
|
32
|
+
## Keep-alive: N links, K + (N−K)
|
|
33
|
+
|
|
34
|
+
After `ensureRuntime`, aim for at least **N** active peer links (subject to `maxActive` / routing profile — "no acquaintances" is never an excuse for N=0):
|
|
35
|
+
|
|
36
|
+
| Slot | Count | Source | Selection |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| Acquaintance | **K** (`K ≤ N`) | `trustedPeers` / high reputation / recent stable peers | High confidence first |
|
|
39
|
+
| Explore | **N−K** | `listVisibleNodeHashes` + PEX/hints | Continuous try; rotate on failure |
|
|
40
|
+
|
|
41
|
+
Explore keeps the node reachable when acquaintances are offline. Stable explore peers (tunable `meshPromoteStableMs`) promote via `promoteExplorePeer` into `trustedPeers`.
|
|
42
|
+
|
|
43
|
+
`transport/mesh_keepalive.mjs`: explore eviction when full; acquaintance rebalance may kick explore slots; proactive close does not sticky re-dial; inbound non-acquaintance peers are tagged explore.
|
|
44
|
+
|
|
45
|
+
### K = 0 (new node / empty trust table)
|
|
46
|
+
|
|
47
|
+
**N−K = N**: automatically `listVisibleNodeHashes` + `connectToNode` on each medium — do not idle waiting for the shell to inject friends.
|
|
48
|
+
|
|
49
|
+
## Trust boundary (linked ≠ trusted)
|
|
50
|
+
|
|
51
|
+
- **Dial / keep-alive / overlay:** connectivity first.
|
|
52
|
+
- **Federation fanout, reputation seeding:** TrustGraph / reputation / denylist; explore slots default low trust.
|
|
53
|
+
- **Inbound payloads:** untrusted ingress — see [AGENTS.md](../AGENTS.md).
|
|
54
|
+
|
|
55
|
+
## Sparse group linking (orthogonal)
|
|
56
|
+
|
|
57
|
+
Within a **known member set**, `group_link_set` dials sparsely for budget (`selectLinkTargetsFromMembers` within `resolveFederationPoolLimits`: top trusted + explore, denylist/quarantine filtered, **anchors always included**). Member discovery uses `listVisibleNodeHashes({ roomSecret })` + `connectToNode` — **not** `subscribe(groupTopic)`.
|
|
58
|
+
|
|
59
|
+
`start()` dials once; membership changes debounce via `notePeerCandidate` (dial newly selected only; never proactive cut — `trimToBudget` is the backstop). This is a budget choice on the member graph, not a ban on linking outside the group.
|
|
60
|
+
|
|
61
|
+
On a sparse group mesh, first-seen valid events forward to federation targets minus sender. Relaying is not a reputation penalty.
|
|
62
|
+
|
|
63
|
+
## See also
|
|
64
|
+
|
|
65
|
+
- [transports.md](transports.md)
|
|
66
|
+
- [signaling.md](signaling.md)
|
|
67
|
+
- [runtime.md](runtime.md)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Subjective reputation
|
|
2
|
+
|
|
3
|
+
Day-to-day package rules: [AGENTS.md](../AGENTS.md). Tunables: `reputation/tunables.json`. Sim co-evolution: [sim/AGENTS.md](../sim/AGENTS.md).
|
|
4
|
+
|
|
5
|
+
## Storage
|
|
6
|
+
|
|
7
|
+
One global score per peer at `{nodeDir}/reputation.json`.
|
|
8
|
+
|
|
9
|
+
## Slash / anti-Sybil
|
|
10
|
+
|
|
11
|
+
- **Subjective slash:** `subjectiveSlashPenalty` — influence scales with sender trust.
|
|
12
|
+
- **Anti-Sybil:** `applyDecayCollusionAfterSlash` after slash / kick / ban.
|
|
13
|
+
- **Safe penalties:** self-observed attributable signals only.
|
|
14
|
+
|
|
15
|
+
## Do not add
|
|
16
|
+
|
|
17
|
+
- Penalties for merely relaying invalid events
|
|
18
|
+
- Penalties for RPC timeouts or empty responses
|
|
19
|
+
|
|
20
|
+
## Fetch ≠ apply
|
|
21
|
+
|
|
22
|
+
`pullReputationFromNode` returns JSON only — never writes. Apply via `setReputationTable` (or equivalent). Infra does not attach `rep_sync` by default — see [infra.md](infra.md).
|
package/docs/runtime.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Runtime bootstrap & lifecycle
|
|
2
|
+
|
|
3
|
+
`ensureRuntime`, startup/shutdown budgets, and Bluetooth hardware probe. Day-to-day shell rules: [AGENTS.md](../AGENTS.md). Providers: [transports.md](transports.md). Mesh N/K: [mesh.md](mesh.md).
|
|
4
|
+
|
|
5
|
+
## `ensureRuntime` contract
|
|
6
|
+
|
|
7
|
+
Returns after registering lan / nostr / bt discovery providers and scheduling background warm — does **not** await lan_tcp listen, Nostr relays, or BT.
|
|
8
|
+
|
|
9
|
+
| Who waits | For what |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Shells (`startNode` / `ensureUserRoom`) | Nothing beyond `ensureRuntime` itself — never read `lanTcpPort` or await public-signaling warm-up |
|
|
12
|
+
| `buildLocalAdvert` / `whenListening` | Local lan_tcp listen only |
|
|
13
|
+
| `ensureLinkToNode` | `whenSignalListening` (Nostr `listenNodeSignals` attached) before dial, so offer/answer does not drop the first signal |
|
|
14
|
+
|
|
15
|
+
Nostr / LAN / BT hooks are progressive. BT discovery / `ble_gatt` always warm in the background.
|
|
16
|
+
|
|
17
|
+
### Fast-listen & dial path
|
|
18
|
+
|
|
19
|
+
- Fast-listen skips providers with `caps.probe: 'native'` (webrtc/ble) — never call their `isAvailable()` on the startup path.
|
|
20
|
+
- Dial path: `canReach` then `isAvailable` per provider (do not `await listAvailableLinkProviders()` first).
|
|
21
|
+
|
|
22
|
+
### Regression budgets
|
|
23
|
+
|
|
24
|
+
| Check | Bound | Test |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| Cold `ensureRuntime` | ≤50ms | `test/pure/startup_budget.test.mjs` |
|
|
27
|
+
| Warm `ensureRuntime` | ≤5ms | same |
|
|
28
|
+
| init → shutdown → natural exit | ≤10s | `test/pure/shutdown_exit.test.mjs` |
|
|
29
|
+
| 10s warm → shutdown → exit | ≤2s | same |
|
|
30
|
+
|
|
31
|
+
Shutdown-exit tests use the production path (default public Nostr + lan). Do not use `relayOverride` / dead-relay crutches there.
|
|
32
|
+
|
|
33
|
+
## Nostr cleanup
|
|
34
|
+
|
|
35
|
+
Use the `ws` package (not global `WebSocket`) — platform WebSocket lacks force-drop / unref needed for clean shutdown. Subscriptions share one WebSocket per relay URL (signal / network advert / per-node advert / group advert multiplex `REQ`s). Active subscriptions reconnect after drop (`NOSTR_RECONNECT_DELAY_MS`) and re-send `REQ`s. On intentional shutdown: `close()`, then `terminate()` after `NOSTR_CLOSE_GRACE_MS` (1s) if the socket is not yet `CLOSED`. Presence publish still uses short-lived sockets.
|
|
36
|
+
|
|
37
|
+
Self presence echo from relays is filtered (`skipNodeHash`, same idea as LAN) and omitted from `listVisibleNodeHashes`. First-seen peer clues notify the link registry (`noteDiscoveryPeerClue`) so dial cooldown unlocks when a peer reappears.
|
|
38
|
+
|
|
39
|
+
## Deno vs Node native addons
|
|
40
|
+
|
|
41
|
+
| Surface | Node | Deno |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| Package tests / production CLI | `node` / `npx @steve02081504/fount-p2p` | Not primary |
|
|
44
|
+
| fount bridge (`npm run test:fount`) | — | `deno.json` keeps `"nodeModulesDir": "none"` |
|
|
45
|
+
| WebRTC | `node-datachannel` when native loads; else pure-JS `node-rtc-connection` (Termux/Android skips native) | same; native needs local `node_modules` + scripts for **only** `node-datachannel` |
|
|
46
|
+
| BLE (`noble` / `bleno`) | optionalDependencies; lazy-loaded | do **not** blanket `--allow-scripts` (optional native builds can abort the whole run) |
|
|
47
|
+
|
|
48
|
+
Recommended Deno one-shot for the published CLI when you want native WebRTC:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
deno run -A --minimum-dependency-age=0 --node-modules-dir=auto --allow-scripts=npm:node-datachannel npm:@steve02081504/fount-p2p
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`deno.json` lists `"allowScripts": ["npm:node-datachannel"]` so project-local `deno install` does not try to compile noble/bleno.
|
|
55
|
+
|
|
56
|
+
On Android/Termux, `node-datachannel` has no Bionic prebuild — loader skips it and uses `node-rtc-connection` (data-channel only; since ≥2.1.0 it exposes native W3C handlers and `bufferedAmount`/`bufferedamountlow` backpressure).
|
|
57
|
+
|
|
58
|
+
## Bluetooth probe
|
|
59
|
+
|
|
60
|
+
`canUseBluetoothRuntime` probes in-process: sysfs hint (Linux) → `loadNoble` → `waitPoweredOn` → `stop()`. Requires `@stoprocent/noble>=2.5.9` on Windows (safe teardown after probe). On failure, discovery/link fall back to other paths.
|
|
61
|
+
|
|
62
|
+
Actual scan/GATT uses the same `loadNoble` / `loadBleno` path. Win defaults to scan-only. Shutdown does not await BT warm; a generation counter invalidates late side effects.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Signaling
|
|
2
|
+
|
|
3
|
+
Internal WebRTC (`needsOfferAnswer`) glare and handshake. Shells use the fount-network API only — [transports.md](transports.md). Discovery / mesh surface: [mesh.md](mesh.md).
|
|
4
|
+
|
|
5
|
+
## Glare: connId dual-PC pick-one
|
|
6
|
+
|
|
7
|
+
`node-datachannel` / `node-rtc-connection` cannot safely host two simultaneous glare dials on one PC. Resolution in `transport/offer_answer.mjs`: both sides dial with a random `connId`; on true glare each side builds an independent answer PC, then **keeps the link initiated by the smaller `nodeHash`** (`linkIsPreferred`). Only the canonical link fires `linkUp` / `linkDown`.
|
|
8
|
+
|
|
9
|
+
- Outbound: `ensureDirectLinkToNode` → `dialOfferAnswer`.
|
|
10
|
+
- Inbound offer with unknown `connId`: new answer PC via `accept` — **not** gated by per-`nodeHash` inflights.
|
|
11
|
+
- One-way dial never builds a second PC.
|
|
12
|
+
|
|
13
|
+
## Handshake: buffer early `auth`
|
|
14
|
+
|
|
15
|
+
Frames: `hello` then `auth`. On simultaneous dial, peer `auth` can arrive before peer `hello` — buffer it (`pendingAuth` in `link/pipe.mjs`); never drop.
|
|
16
|
+
|
|
17
|
+
## Windows / `trickleIceOff`
|
|
18
|
+
|
|
19
|
+
When set: send final offer/answer after ICE gathering, dedupe remote signals, queue remote ICE until both descriptions are ready.
|
|
20
|
+
|
|
21
|
+
## Runtime relay override
|
|
22
|
+
|
|
23
|
+
`setSignalingRuntimeConfig({ relayOverride, iceLocalHostnamePolicy, trickleIceOff })` after `initNode` (or pass `signaling` once on first `startNode`). `relayOverride` **replaces** the default public relay list (do not merge defaults back in). Changes emit `signaling-changed` and trigger `reloadDiscoveryRelays` (swap Nostr provider + rebind node presence/signals).
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# fount network vs internal transports
|
|
2
|
+
|
|
3
|
+
Mesh keep-alive / discovery: [mesh.md](mesh.md). WebRTC glare / handshake: [signaling.md](signaling.md). Runtime lifecycle: [runtime.md](runtime.md).
|
|
4
|
+
|
|
5
|
+
## No versioning
|
|
6
|
+
|
|
7
|
+
Do **not** introduce version fields, constants, or suffixes (`v`, `version`, `FRAME_VERSION`, `:v1`, …). Changing a shape means changing it; no dual-read / backward-compat paths. Exception: npm `package.json` `version` is for package publish only.
|
|
8
|
+
|
|
9
|
+
## Public contract (shell / L4)
|
|
10
|
+
|
|
11
|
+
This package exposes a **fount network**: talk to `nodeHash` peers with envelopes.
|
|
12
|
+
|
|
13
|
+
Typical entrypoints:
|
|
14
|
+
|
|
15
|
+
- `startNode` / `getLinkRegistry().ensureRuntime()`
|
|
16
|
+
- `ensureLinkToNode` / `sendToNodeLink` / `subscribeScope`
|
|
17
|
+
- `createGroupLinkSet` / `createScopedLinkRoom` / `ensureUserRoom`
|
|
18
|
+
|
|
19
|
+
`registerScopeAuthorizer` may be called before `initNode` / `getLinkRegistry` — it only buffers policy until the default registry is created.
|
|
20
|
+
|
|
21
|
+
Callers do **not** choose WebRTC, BLE, ICE, or DataChannels. If a path is unavailable on the host, the registry tries the next internal provider; the API surface stays the same.
|
|
22
|
+
|
|
23
|
+
**Public registration only:** `registerLinkProvider` / `registerDiscoveryProvider` from the package facade or `@steve02081504/fount-p2p/link` / `./discovery`. Provider *implementations* under `link/providers/*` remain package-internal — shells must not import them or pick transports.
|
|
24
|
+
|
|
25
|
+
Public `./transport/*` subpaths: `link_registry`, `user_room`, `group_link_set`, `node_scope/wire`, `node_scope/features`, `room_scopes`, `remote_user_room`, `scoped_link`. There is no `transport/node_scope` barrel — import the concrete modules. Modules such as `offer_answer`, `runtime_bootstrap` are internal.
|
|
26
|
+
|
|
27
|
+
Topic / rendezvous / signal crypto live under `discovery/` (`nostr.mjs`, `internal/signal_crypto.mjs`, `adverts.mjs`) — not in `transport/`, not a package export. Do not export `advertiseTopic` / `subscribeTopic` / `sendSignal(topic)` on the fount-network surface. ICE `.local` host-candidate filtering is `iceLocalHostnamePolicy` only.
|
|
28
|
+
|
|
29
|
+
## Internal layers
|
|
30
|
+
|
|
31
|
+
| Layer | Role |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| **Discovery** (`discovery/`) | Per medium: `listVisibleNodeHashes` + `connectToNode`. Encrypted adverts/signals via `adverts.mjs` + `index.mjs` helpers. |
|
|
34
|
+
| **Link providers** (`link/providers/`) | Open a duplex pipe; sorted by **`level` (descending)** — not a shell import |
|
|
35
|
+
| **Registry** (`transport/link_registry.mjs`) | fount-network facade: dial fallback, scope/overlay, one canonical link per peer |
|
|
36
|
+
| **Bootstrap** (`transport/runtime_bootstrap.mjs`) | `ensureRuntime` register + progressive listen/discovery/BT warm; `reloadDiscoveryRelays` on `signaling-changed` |
|
|
37
|
+
| **Offer/answer** (`transport/offer_answer.mjs`) | Discovery-signal glare path for `caps.needsOfferAnswer` (**internal**; uses `sendNodeSignalPacket`) |
|
|
38
|
+
| **Mesh keepalive** (`transport/mesh_keepalive.mjs`) | N/K pool, explore eviction, stable promote to `trustedPeers` |
|
|
39
|
+
|
|
40
|
+
LinkHandle for upper layers: `ready` / `nodeHash` / `send` / `onEnvelope` / `onDown` / `close` / `stats`. Transport-specific fields are for in-package scheduling only.
|
|
41
|
+
|
|
42
|
+
Provider optional hooks (package-internal): `ensureListening`, `localEndpoint`, `canReach`, `caps.probe: 'sync' | 'native'` (`native` = skipped on ensureRuntime fast-listen). Discovery `connectToNode` / `sendNodeSignal` may return `false` when the path is unavailable; fan-out treats that as silent skip. Per-provider throw/false in discovery and link dial fallback are silent; only total failure of the abstraction surfaces to the caller.
|
|
43
|
+
|
|
44
|
+
Each registry only calls `ensureListening` on **its own** `lan_tcp` / `ble_gatt` instances (unique registry ids like `lan_tcp:ab12cd34`). Never fan out listening to other registries' sockets.
|
|
45
|
+
|
|
46
|
+
Chain `providerId` on the LinkHandle stays the short name (`lan_tcp` / `ble_gatt` / `webrtc` / `nostr`) for scheduling/stats.
|
|
47
|
+
|
|
48
|
+
## Level table
|
|
49
|
+
|
|
50
|
+
| id | level |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `lan_tcp` | 80 |
|
|
53
|
+
| `webrtc` | 70 |
|
|
54
|
+
| `ble_gatt` | 40 |
|
|
55
|
+
| `nostr` | −∞ |
|
|
56
|
+
|
|
57
|
+
Constants: `link/providers/levels.mjs`. Discovery uses ascending **`priority`** (handshake / presence / signal media order — Nostr stays last at `100`). Link selection uses descending **`level`** (data transport); Nostr is −∞ so it is dialed only after LAN / WebRTC / BLE fail.
|
|
58
|
+
|
|
59
|
+
## Fallback
|
|
60
|
+
|
|
61
|
+
1. `canReach` false → skip (no dial)
|
|
62
|
+
2. `isAvailable()` fails → skip (probed per provider on the dial path; never via `listAvailableLinkProviders()` first)
|
|
63
|
+
3. dial/handshake fails or soft-fails (`null`) → next lower level
|
|
64
|
+
4. races: higher `level` wins; same level → smaller `nodeHash` initiates
|
|
65
|
+
|
|
66
|
+
`caps.needsOfferAnswer` providers use the shared discovery-signal glare path (`dial`/`accept` + signal session) — not hard-coded to `id === 'webrtc'`.
|
|
67
|
+
|
|
68
|
+
Dial miss / exhausted peers get exponential cooldown so mesh ticks do not busy-loop on stale acquaintances. First-seen discovery peer clues (and `watchNodeAdvert` ingest) clear that peer's cooldown.
|
|
69
|
+
|
|
70
|
+
## Providers (internal)
|
|
71
|
+
|
|
72
|
+
### `lan_tcp` (80)
|
|
73
|
+
|
|
74
|
+
Plain TCP on the LAN. Registry schedules listen in the background after `ensureRuntime`; `buildLocalAdvert` waits for local listen so signed adverts include `tcpPort`. Peers learn `{ host, port }` from discovery meta + advert `tcpPort`. Binding = shared `linkId`; length-prefix framing. No discovery signal / offer-answer. Shells never read `tcpPort`.
|
|
75
|
+
|
|
76
|
+
### `webrtc` (70)
|
|
77
|
+
|
|
78
|
+
Discovery signal + dual DataChannel; DTLS fingerprint as handshake binding; `needsOfferAnswer` glare path. Soft-fail (`null`) continues to lower-level providers. Backend: `node-datachannel` when the native addon loads, else pure-JS `node-rtc-connection` (Android/Termux skips native). See [runtime.md](runtime.md).
|
|
79
|
+
|
|
80
|
+
### `ble_gatt` (40)
|
|
81
|
+
|
|
82
|
+
GATT write/notify; binding = shared `linkId`; needs BT peer hint (`peripheralId` in discovery meta); optional noble/bleno. Per-registry instance like `lan_tcp`; `isAvailable` / `canReach` gate dial. On Win32, scan-only stacks cannot accept inbound BLE links. One BLE adapter cannot host two independent peripherals in-process — production is one node per process. Hardware probe: [runtime.md](runtime.md).
|
|
83
|
+
|
|
84
|
+
### `nostr` (−∞)
|
|
85
|
+
|
|
86
|
+
Last-resort duplex pipe over discovery signal packets (`type: 'link'`), demuxed from WebRTC `type: 'signal'`. Same node rendezvous encryption as signaling; not a second relay subscription. Longer handshake / heartbeat / idle than LAN. Discovery **`priority`** is unchanged (still handshake-last); only link **`level`** is −∞.
|
|
87
|
+
|
|
88
|
+
### Bluetooth discovery signal
|
|
89
|
+
|
|
90
|
+
`discovery/bt` carries short signal blobs on GATT so WebRTC can negotiate near-field when LAN/nostr are unavailable (package-internal).
|
|
91
|
+
|
|
92
|
+
Discovery peripheral and `ble_gatt` both use bleno + name `fount-bt`. On one adapter they contend — last `setServices`/`startAdvertising` wins. Production: one node per process.
|
package/docs/wire.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Wire part / fanout attaches
|
|
2
|
+
|
|
3
|
+
Day-to-day trust rules: [AGENTS.md](../AGENTS.md). Node-scope presets: [infra.md](infra.md).
|
|
4
|
+
|
|
5
|
+
## Fanout vs targeted (attach inventory)
|
|
6
|
+
|
|
7
|
+
| Use | API | Notes |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| Timeline / chunk exploration | `fanoutToTopNodes` | TrustGraph-ranked fanout |
|
|
10
|
+
| Mailbox / targeted packets | `sendToNode` / User Room | Never fanout |
|
|
11
|
+
| part_invoke RPC collect | `wire/part/fanout.collectPartInvokeResponses` | Requires `attachPartWire` already; `timeoutMs` bounds end-to-end |
|
|
12
|
+
| Group-room part | `wire/part/group.attachGroupPartWire` | Group federation frames |
|
|
13
|
+
| TrustGraph / group Trystero chunk | `files/chunk/responder.attachTrustGraphFedChunkResponder` | Chunk responder on trust/group path |
|
|
14
|
+
|
|
15
|
+
Part query runtime lives under `federation/part_query/*`; wire attach only in `wire/part/query.mjs`.
|
|
16
|
+
|
|
17
|
+
## Timed collect
|
|
18
|
+
|
|
19
|
+
APIs with `timeoutMs` (e.g. `collectPartInvokeResponses`) must register the wait **first** and must **not** `await` fanout/send on the return path — a stuck `discoverRoute` / `link.send` otherwise defeats the timeout.
|
|
20
|
+
|
|
21
|
+
Pattern: `beginFedFanoutFetch` / fire-and-forget fanout + `sent === 0 → finish()`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steve02081504/fount-p2p",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.32",
|
|
4
4
|
"description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -23,13 +23,6 @@
|
|
|
23
23
|
"test:live": "node --test --test-concurrency=1 --test-force-exit test/live/*.test.mjs",
|
|
24
24
|
"test:sim": "node --test --test-concurrency=1 --test-force-exit sim/test/*.test.mjs"
|
|
25
25
|
},
|
|
26
|
-
"files": [
|
|
27
|
-
"**/*.mjs",
|
|
28
|
-
"**/*.json",
|
|
29
|
-
"!test/**",
|
|
30
|
-
"!sim/**",
|
|
31
|
-
"!scripts/**"
|
|
32
|
-
],
|
|
33
26
|
"bin": {
|
|
34
27
|
"fount-p2p": "infra/cli.mjs"
|
|
35
28
|
},
|
|
@@ -82,7 +75,7 @@
|
|
|
82
75
|
"optionalDependencies": {
|
|
83
76
|
"@stoprocent/bleno": "latest",
|
|
84
77
|
"@stoprocent/noble": "latest",
|
|
85
|
-
"node-datachannel": "
|
|
78
|
+
"node-datachannel": "0.33.0"
|
|
86
79
|
},
|
|
87
80
|
"allowScripts": {
|
|
88
81
|
"node-datachannel": true
|
package/sim/AGENTS.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: P2P tunables simulation / co-evolution harness — fidelity boundary between reused real logic and heuristic proxy environment
|
|
3
|
+
globs: sim/**
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# P2P Sim Harness Guide
|
|
8
|
+
|
|
9
|
+
In-process co-evolution of **tunables** (`*.tunables.json`) against an **attack genome**, scored by `metrics.mjs`. Search proxy — **not** wire-protocol replay.
|
|
10
|
+
|
|
11
|
+
## Fidelity boundary
|
|
12
|
+
|
|
13
|
+
- **Reused verbatim** (import real decision functions; do not re-model): `reputation/engine.mjs`, `reputation/math.mjs`, `sim/social_reputation.mjs` (`*Pure`), `trust_graph/engine.mjs` (`pickTop`), `trust_graph/resolve.mjs`, `governance/join_pow.mjs`.
|
|
14
|
+
- **Tunables source:** `tunables_bundle.mjs` loads in-package `*.tunables.json` plus `sim/reputation_social.tunables.json`. Write social tunables back via `cli.mjs mine --social-tunables PATH` (`apply.mjs`).
|
|
15
|
+
- **`PARAM_SPACE` ↔ defaults:** every `PARAM_SPACE` key must exist in `loadDefaultTunables()`; clear both sides when deleting a key.
|
|
16
|
+
- **`socialRepHideThreshold`:** hide when `score < threshold`. Default `0` (suppress negatives only). Raise the threshold to raise `falsePositiveRate` — never use a negative threshold for "stricter".
|
|
17
|
+
- **Heuristic proxy** (not the real path): `model.mjs`, `discovery.mjs`, `transport.mjs`, `integrity.mjs` — analytical "params → defense" only.
|
|
18
|
+
- **Mesh-first:** explore slots and cold-start join are mandatory; fanout/trust stay separate from link presence. Policy: [docs/mesh.md](../docs/mesh.md). Wiring: [cold_start.md](cold_start.md).
|
|
19
|
+
|
|
20
|
+
## Anti-drift
|
|
21
|
+
|
|
22
|
+
- Do not hand-copy runtime constants. RTC budget from `transport/rtc_connection_budget.mjs`. `EXPLORE_MAX_PER_SOURCE` mirrors `peer_pool.mjs` but stays local (importing `peer_pool` pulls fs) — `test/fidelity.test.mjs` asserts equality.
|
|
23
|
+
- Signaling source names (`DEFAULT_SIGNALING_SOURCES`) must be real provider ids (`lan` / `nostr` / `bt`). No `tracker` provider.
|
|
24
|
+
- New sim constants that shadow real ones need a matching assertion in `fidelity.test.mjs`.
|
|
25
|
+
|
|
26
|
+
## Determinism
|
|
27
|
+
|
|
28
|
+
- Seeded via `rng.mjs` (`createRng`). `runSimulation(...)` must be pure — `fidelity.test.mjs` asserts serial == parallel == batched snapshots. Use `simulationContext.now` (virtual clock, +60s/round); no wall-clock or unseeded RNG.
|
|
29
|
+
- Round state object is `simulationContext` (not `ctx`); `buildWorld` returns `{ simulationContext }`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 入群 PoW 成本模型:算力预算限速 Sybil/洗白造号。
|
|
3
|
+
*/
|
|
4
|
+
import { expectedJoinPowHashes, powVoluntaryBonus } from '../governance/join_pow.mjs'
|
|
5
|
+
|
|
6
|
+
/** 每回合可消耗的期望哈希预算(相对 2^16 基准) */
|
|
7
|
+
const HASH_BUDGET_PER_ROUND = 65536
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {import('./tunables_bundle.mjs').TunablesBundle['admission']} admission admission tunables
|
|
11
|
+
* @returns {number} 准入 floor bit
|
|
12
|
+
*/
|
|
13
|
+
export function resolvePowFloorBits(admission) {
|
|
14
|
+
return Math.max(1, Math.floor(Number(admission?.powFloorBits ?? admission?.powDifficultyBits ?? 18)))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {number} achievedBits 解达成 bit
|
|
19
|
+
* @param {import('./tunables_bundle.mjs').TunablesBundle['admission']} admission admission tunables
|
|
20
|
+
* @returns {number} 自愿封顶信誉加成
|
|
21
|
+
*/
|
|
22
|
+
export function simPowVoluntaryBonus(achievedBits, admission) {
|
|
23
|
+
return powVoluntaryBonus(achievedBits, resolvePowFloorBits(admission), admission)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {number} difficultyBits PoW floor bit
|
|
28
|
+
* @returns {number} 造一个新身份期望回合成本
|
|
29
|
+
*/
|
|
30
|
+
export function roundsPerIdentity(difficultyBits) {
|
|
31
|
+
const expected = expectedJoinPowHashes(difficultyBits)
|
|
32
|
+
return Math.max(1, expected / HASH_BUDGET_PER_ROUND)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {number} requested 请求数量
|
|
37
|
+
* @param {number} difficultyBits PoW 难度
|
|
38
|
+
* @param {number} rounds 仿真总回合
|
|
39
|
+
* @returns {number} 预算内可生成的恶意身份数
|
|
40
|
+
*/
|
|
41
|
+
export function capMaliciousByPowBudget(requested, difficultyBits, rounds) {
|
|
42
|
+
const cost = roundsPerIdentity(difficultyBits)
|
|
43
|
+
const budget = Math.max(1, Math.floor(rounds / cost))
|
|
44
|
+
return Math.min(requested, budget)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {number} difficultyBits PoW 难度
|
|
49
|
+
* @param {number} rounds 回合数
|
|
50
|
+
* @returns {number} 诚实新人入群延迟惩罚 0..1(越高=越慢)
|
|
51
|
+
*/
|
|
52
|
+
export function honestJoinDelayPenalty(difficultyBits, rounds) {
|
|
53
|
+
const cost = roundsPerIdentity(difficultyBits)
|
|
54
|
+
const delay = cost / Math.max(1, rounds * 0.25)
|
|
55
|
+
return Math.min(1, delay)
|
|
56
|
+
}
|
package/sim/anomaly.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 仿真器中的本地可逆异常隔离(映射生产 reputation_engine 纯函数)。
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
isQuarantinedPure,
|
|
6
|
+
observeBehaviorSamplePure,
|
|
7
|
+
} from '../reputation/engine.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {import('../node/reputation_store.mjs').ReputationFile} reputation 观察者信誉表
|
|
11
|
+
* @param {string} peerId 对端 nodeHash
|
|
12
|
+
* @param {number} sample 行为样本
|
|
13
|
+
* @param {number} now 当前时间
|
|
14
|
+
* @param {import('../reputation/tunables.json')} tunables reputation tunables
|
|
15
|
+
* @returns {boolean} 是否触发隔离
|
|
16
|
+
*/
|
|
17
|
+
export function simObservePeerBehavior(reputation, peerId, sample, now, tunables) {
|
|
18
|
+
return observeBehaviorSamplePure(reputation, peerId, sample, now, tunables).anomaly
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {import('../node/reputation_store.mjs').ReputationFile} reputation 观察者信誉表
|
|
23
|
+
* @param {string} peerId 对端 nodeHash
|
|
24
|
+
* @param {number} now 当前时间
|
|
25
|
+
* @returns {boolean} 是否隔离中
|
|
26
|
+
*/
|
|
27
|
+
export function simIsPeerQuarantined(reputation, peerId, now) {
|
|
28
|
+
return isQuarantinedPure(reputation, peerId, now)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {number} discoveryReach 发现可达 0..1
|
|
33
|
+
* @param {() => number} rng 随机源
|
|
34
|
+
* @returns {boolean} 攻击是否触达该观察者
|
|
35
|
+
*/
|
|
36
|
+
export function attackReachesObserver(discoveryReach, rng) {
|
|
37
|
+
const reach = Math.max(0, Math.min(1, discoveryReach))
|
|
38
|
+
return rng() < 0.15 + reach * 0.85
|
|
39
|
+
}
|