@optimystic/db-p2p 0.13.5 → 0.14.1
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/README.md +6 -6
- package/dist/src/cluster/block-transfer-service.d.ts +67 -8
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +101 -62
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/client.d.ts +18 -1
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +55 -39
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-error.d.ts +38 -0
- package/dist/src/cluster/cluster-error.d.ts.map +1 -0
- package/dist/src/cluster/cluster-error.js +51 -0
- package/dist/src/cluster/cluster-error.js.map +1 -0
- package/dist/src/cluster/cluster-repo.d.ts +135 -3
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +333 -58
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-cert.d.ts +63 -0
- package/dist/src/cluster/commit-cert.d.ts.map +1 -0
- package/dist/src/cluster/commit-cert.js +108 -0
- package/dist/src/cluster/commit-cert.js.map +1 -0
- package/dist/src/cluster/service.d.ts +46 -11
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +97 -33
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +13 -0
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +34 -4
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/bootstrap-evidence-builder.d.ts +59 -0
- package/dist/src/cohort-topic/bootstrap-evidence-builder.d.ts.map +1 -0
- package/dist/src/cohort-topic/bootstrap-evidence-builder.js +79 -0
- package/dist/src/cohort-topic/bootstrap-evidence-builder.js.map +1 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.d.ts +81 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.d.ts.map +1 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.js +93 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.js.map +1 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.d.ts +108 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.d.ts.map +1 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.js +108 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.js.map +1 -0
- package/dist/src/cohort-topic/change-bridge.d.ts +65 -0
- package/dist/src/cohort-topic/change-bridge.d.ts.map +1 -0
- package/dist/src/cohort-topic/change-bridge.js +76 -0
- package/dist/src/cohort-topic/change-bridge.js.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts +74 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.js +87 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.js.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts +42 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.js +57 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.js.map +1 -0
- package/dist/src/cohort-topic/fret-trust-anchor.d.ts +100 -0
- package/dist/src/cohort-topic/fret-trust-anchor.d.ts.map +1 -0
- package/dist/src/cohort-topic/fret-trust-anchor.js +115 -0
- package/dist/src/cohort-topic/fret-trust-anchor.js.map +1 -0
- package/dist/src/cohort-topic/host.d.ts +573 -0
- package/dist/src/cohort-topic/host.d.ts.map +1 -0
- package/dist/src/cohort-topic/host.js +1460 -0
- package/dist/src/cohort-topic/host.js.map +1 -0
- package/dist/src/cohort-topic/index.d.ts +14 -0
- package/dist/src/cohort-topic/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/index.js +14 -0
- package/dist/src/cohort-topic/index.js.map +1 -0
- package/dist/src/cohort-topic/membership-publish-sink.d.ts +14 -0
- package/dist/src/cohort-topic/membership-publish-sink.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership-publish-sink.js +17 -0
- package/dist/src/cohort-topic/membership-publish-sink.js.map +1 -0
- package/dist/src/cohort-topic/membership-source.d.ts +36 -0
- package/dist/src/cohort-topic/membership-source.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership-source.js +58 -0
- package/dist/src/cohort-topic/membership-source.js.map +1 -0
- package/dist/src/cohort-topic/peer-codec.d.ts +18 -0
- package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -0
- package/dist/src/cohort-topic/peer-codec.js +26 -0
- package/dist/src/cohort-topic/peer-codec.js.map +1 -0
- package/dist/src/cohort-topic/peer-sig.d.ts +52 -0
- package/dist/src/cohort-topic/peer-sig.d.ts.map +1 -0
- package/dist/src/cohort-topic/peer-sig.js +83 -0
- package/dist/src/cohort-topic/peer-sig.js.map +1 -0
- package/dist/src/cohort-topic/protocols.d.ts +49 -0
- package/dist/src/cohort-topic/protocols.d.ts.map +1 -0
- package/dist/src/cohort-topic/protocols.js +58 -0
- package/dist/src/cohort-topic/protocols.js.map +1 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.d.ts +49 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.d.ts.map +1 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.js +42 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.js.map +1 -0
- package/dist/src/cohort-topic/size-estimator.d.ts +16 -0
- package/dist/src/cohort-topic/size-estimator.d.ts.map +1 -0
- package/dist/src/cohort-topic/size-estimator.js +16 -0
- package/dist/src/cohort-topic/size-estimator.js.map +1 -0
- package/dist/src/cohort-topic/stream-util.d.ts +19 -0
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -0
- package/dist/src/cohort-topic/stream-util.js +76 -0
- package/dist/src/cohort-topic/stream-util.js.map +1 -0
- package/dist/src/cohort-topic/threshold-crypto.d.ts +94 -0
- package/dist/src/cohort-topic/threshold-crypto.d.ts.map +1 -0
- package/dist/src/cohort-topic/threshold-crypto.js +178 -0
- package/dist/src/cohort-topic/threshold-crypto.js.map +1 -0
- package/dist/src/cohort-topic/topic-router.d.ts +41 -0
- package/dist/src/cohort-topic/topic-router.d.ts.map +1 -0
- package/dist/src/cohort-topic/topic-router.js +63 -0
- package/dist/src/cohort-topic/topic-router.js.map +1 -0
- package/dist/src/dispute/cascade.d.ts +179 -0
- package/dist/src/dispute/cascade.d.ts.map +1 -0
- package/dist/src/dispute/cascade.js +315 -0
- package/dist/src/dispute/cascade.js.map +1 -0
- package/dist/src/dispute/client.d.ts +2 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +12 -3
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +41 -1
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +147 -21
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/index.d.ts +2 -0
- package/dist/src/dispute/index.d.ts.map +1 -1
- package/dist/src/dispute/index.js +2 -0
- package/dist/src/dispute/index.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +266 -0
- package/dist/src/dispute/invalidation.d.ts.map +1 -0
- package/dist/src/dispute/invalidation.js +405 -0
- package/dist/src/dispute/invalidation.js.map +1 -0
- package/dist/src/dispute/types.d.ts +14 -0
- package/dist/src/dispute/types.d.ts.map +1 -1
- package/dist/src/dispute/types.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +7 -0
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +49 -5
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +45 -3
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +522 -5
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/matchmaking/aggregate-counts.d.ts +67 -0
- package/dist/src/matchmaking/aggregate-counts.d.ts.map +1 -0
- package/dist/src/matchmaking/aggregate-counts.js +60 -0
- package/dist/src/matchmaking/aggregate-counts.js.map +1 -0
- package/dist/src/matchmaking/index.d.ts +20 -0
- package/dist/src/matchmaking/index.d.ts.map +1 -0
- package/dist/src/matchmaking/index.js +20 -0
- package/dist/src/matchmaking/index.js.map +1 -0
- package/dist/src/matchmaking/module.d.ts +181 -0
- package/dist/src/matchmaking/module.d.ts.map +1 -0
- package/dist/src/matchmaking/module.js +221 -0
- package/dist/src/matchmaking/module.js.map +1 -0
- package/dist/src/matchmaking/protocols.d.ts +37 -0
- package/dist/src/matchmaking/protocols.d.ts.map +1 -0
- package/dist/src/matchmaking/protocols.js +42 -0
- package/dist/src/matchmaking/protocols.js.map +1 -0
- package/dist/src/matchmaking/provider-manager.d.ts +52 -0
- package/dist/src/matchmaking/provider-manager.d.ts.map +1 -0
- package/dist/src/matchmaking/provider-manager.js +74 -0
- package/dist/src/matchmaking/provider-manager.js.map +1 -0
- package/dist/src/matchmaking/query-handler.d.ts +34 -0
- package/dist/src/matchmaking/query-handler.d.ts.map +1 -0
- package/dist/src/matchmaking/query-handler.js +62 -0
- package/dist/src/matchmaking/query-handler.js.map +1 -0
- package/dist/src/matchmaking/query-transport.d.ts +180 -0
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -0
- package/dist/src/matchmaking/query-transport.js +315 -0
- package/dist/src/matchmaking/query-transport.js.map +1 -0
- package/dist/src/matchmaking/seeker-manager.d.ts +39 -0
- package/dist/src/matchmaking/seeker-manager.d.ts.map +1 -0
- package/dist/src/matchmaking/seeker-manager.js +49 -0
- package/dist/src/matchmaking/seeker-manager.js.map +1 -0
- package/dist/src/matchmaking/seeker-walk-client.d.ts +127 -0
- package/dist/src/matchmaking/seeker-walk-client.d.ts.map +1 -0
- package/dist/src/matchmaking/seeker-walk-client.js +189 -0
- package/dist/src/matchmaking/seeker-walk-client.js.map +1 -0
- package/dist/src/matchmaking/traffic-validation.d.ts +128 -0
- package/dist/src/matchmaking/traffic-validation.d.ts.map +1 -0
- package/dist/src/matchmaking/traffic-validation.js +105 -0
- package/dist/src/matchmaking/traffic-validation.js.map +1 -0
- package/dist/src/network/network-manager-service.d.ts +4 -2
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +9 -3
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/protocol-client.d.ts +13 -0
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +74 -2
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/reactivity/forwarder-host.d.ts +198 -0
- package/dist/src/reactivity/forwarder-host.d.ts.map +1 -0
- package/dist/src/reactivity/forwarder-host.js +355 -0
- package/dist/src/reactivity/forwarder-host.js.map +1 -0
- package/dist/src/reactivity/index.d.ts +19 -0
- package/dist/src/reactivity/index.d.ts.map +1 -0
- package/dist/src/reactivity/index.js +19 -0
- package/dist/src/reactivity/index.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts +70 -0
- package/dist/src/reactivity/notify-transport.d.ts.map +1 -0
- package/dist/src/reactivity/notify-transport.js +111 -0
- package/dist/src/reactivity/notify-transport.js.map +1 -0
- package/dist/src/reactivity/origination-manager.d.ts +110 -0
- package/dist/src/reactivity/origination-manager.d.ts.map +1 -0
- package/dist/src/reactivity/origination-manager.js +129 -0
- package/dist/src/reactivity/origination-manager.js.map +1 -0
- package/dist/src/reactivity/protocols.d.ts +43 -0
- package/dist/src/reactivity/protocols.d.ts.map +1 -0
- package/dist/src/reactivity/protocols.js +50 -0
- package/dist/src/reactivity/protocols.js.map +1 -0
- package/dist/src/reactivity/push-state-gossip.d.ts +132 -0
- package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -0
- package/dist/src/reactivity/push-state-gossip.js +231 -0
- package/dist/src/reactivity/push-state-gossip.js.map +1 -0
- package/dist/src/reactivity/recover-transport.d.ts +167 -0
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -0
- package/dist/src/reactivity/recover-transport.js +308 -0
- package/dist/src/reactivity/recover-transport.js.map +1 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.d.ts +142 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.d.ts.map +1 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.js +223 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.js.map +1 -0
- package/dist/src/reactivity/subscriber-registry.d.ts +47 -0
- package/dist/src/reactivity/subscriber-registry.d.ts.map +1 -0
- package/dist/src/reactivity/subscriber-registry.js +87 -0
- package/dist/src/reactivity/subscriber-registry.js.map +1 -0
- package/dist/src/reactivity/subscription-manager.d.ts +221 -0
- package/dist/src/reactivity/subscription-manager.d.ts.map +1 -0
- package/dist/src/reactivity/subscription-manager.js +302 -0
- package/dist/src/reactivity/subscription-manager.js.map +1 -0
- package/dist/src/reactivity/topic-bytes.d.ts +32 -0
- package/dist/src/reactivity/topic-bytes.d.ts.map +1 -0
- package/dist/src/reactivity/topic-bytes.js +34 -0
- package/dist/src/reactivity/topic-bytes.js.map +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +51 -23
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/service.d.ts +46 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +79 -45
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/rpc-deadline.d.ts +37 -0
- package/dist/src/rpc-deadline.d.ts.map +1 -0
- package/dist/src/rpc-deadline.js +31 -0
- package/dist/src/rpc-deadline.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +2 -0
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +95 -2
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +25 -0
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +53 -2
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +193 -9
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/client.d.ts +4 -1
- package/dist/src/sync/client.d.ts.map +1 -1
- package/dist/src/sync/client.js +5 -2
- package/dist/src/sync/client.js.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +219 -0
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/cohort-topic-mesh-harness.js +467 -0
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -0
- package/dist/src/testing/matchmaking-mesh-harness.d.ts +187 -0
- package/dist/src/testing/matchmaking-mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/matchmaking-mesh-harness.js +358 -0
- package/dist/src/testing/matchmaking-mesh-harness.js.map +1 -0
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +23 -1
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/reactivity-mesh-harness.d.ts +298 -0
- package/dist/src/testing/reactivity-mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/reactivity-mesh-harness.js +663 -0
- package/dist/src/testing/reactivity-mesh-harness.js.map +1 -0
- package/package.json +7 -4
- package/src/cluster/block-transfer-service.ts +133 -68
- package/src/cluster/client.ts +64 -45
- package/src/cluster/cluster-error.ts +64 -0
- package/src/cluster/cluster-repo.ts +405 -55
- package/src/cluster/commit-cert.ts +139 -0
- package/src/cluster/service.ts +116 -44
- package/src/cluster/spread-on-churn.ts +328 -285
- package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -0
- package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -0
- package/src/cohort-topic/bootstrap-parent-reference.ts +159 -0
- package/src/cohort-topic/change-bridge.ts +109 -0
- package/src/cohort-topic/cohort-gossip-driver.ts +144 -0
- package/src/cohort-topic/cohort-gossip-transport.ts +74 -0
- package/src/cohort-topic/fret-trust-anchor.ts +153 -0
- package/src/cohort-topic/host.ts +2175 -0
- package/src/cohort-topic/index.ts +13 -0
- package/src/cohort-topic/membership-publish-sink.ts +20 -0
- package/src/cohort-topic/membership-source.ts +68 -0
- package/src/cohort-topic/peer-codec.ts +31 -0
- package/src/cohort-topic/peer-sig.ts +86 -0
- package/src/cohort-topic/protocols.ts +71 -0
- package/src/cohort-topic/reactivity-membership-gate.ts +77 -0
- package/src/cohort-topic/size-estimator.ts +16 -0
- package/src/cohort-topic/stream-util.ts +87 -0
- package/src/cohort-topic/threshold-crypto.ts +239 -0
- package/src/cohort-topic/topic-router.ts +77 -0
- package/src/dispute/cascade.ts +517 -0
- package/src/dispute/client.ts +12 -2
- package/src/dispute/dispute-service.ts +171 -24
- package/src/dispute/index.ts +42 -0
- package/src/dispute/invalidation.ts +587 -0
- package/src/dispute/types.ts +14 -0
- package/src/index.ts +4 -0
- package/src/libp2p-key-network.ts +49 -6
- package/src/libp2p-node-base.ts +620 -12
- package/src/matchmaking/aggregate-counts.ts +104 -0
- package/src/matchmaking/index.ts +20 -0
- package/src/matchmaking/module.ts +363 -0
- package/src/matchmaking/protocols.ts +51 -0
- package/src/matchmaking/provider-manager.ts +95 -0
- package/src/matchmaking/query-handler.ts +88 -0
- package/src/matchmaking/query-transport.ts +483 -0
- package/src/matchmaking/seeker-manager.ts +64 -0
- package/src/matchmaking/seeker-walk-client.ts +293 -0
- package/src/matchmaking/traffic-validation.ts +195 -0
- package/src/network/network-manager-service.ts +9 -2
- package/src/protocol-client.ts +64 -3
- package/src/reactivity/forwarder-host.ts +438 -0
- package/src/reactivity/index.ts +19 -0
- package/src/reactivity/notify-transport.ts +144 -0
- package/src/reactivity/origination-manager.ts +192 -0
- package/src/reactivity/protocols.ts +61 -0
- package/src/reactivity/push-state-gossip.ts +291 -0
- package/src/reactivity/recover-transport.ts +408 -0
- package/src/reactivity/rotation-rereg-scheduler.ts +256 -0
- package/src/reactivity/subscriber-registry.ts +96 -0
- package/src/reactivity/subscription-manager.ts +450 -0
- package/src/reactivity/topic-bytes.ts +37 -0
- package/src/repo/client.ts +54 -23
- package/src/repo/service.ts +90 -42
- package/src/rpc-deadline.ts +45 -0
- package/src/storage/block-storage.ts +106 -3
- package/src/storage/i-block-storage.ts +27 -0
- package/src/storage/storage-repo.ts +209 -13
- package/src/sync/client.ts +5 -2
- package/src/testing/cohort-topic-mesh-harness.ts +613 -0
- package/src/testing/matchmaking-mesh-harness.ts +475 -0
- package/src/testing/mesh-harness.ts +22 -2
- package/src/testing/reactivity-mesh-harness.ts +922 -0
package/src/libp2p-node-base.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { noise } from '@chainsafe/libp2p-noise';
|
|
|
3
3
|
import { yamux } from '@chainsafe/libp2p-yamux';
|
|
4
4
|
import { identify } from '@libp2p/identify';
|
|
5
5
|
import { ping } from '@libp2p/ping';
|
|
6
|
+
import { dcutr } from '@libp2p/dcutr';
|
|
7
|
+
import { autoNAT } from '@libp2p/autonat';
|
|
6
8
|
import { gossipsub } from '@chainsafe/libp2p-gossipsub';
|
|
7
9
|
import { bootstrap } from '@libp2p/bootstrap';
|
|
8
10
|
import { circuitRelayServer, type CircuitRelayServerInit } from '@libp2p/circuit-relay-v2';
|
|
@@ -10,30 +12,70 @@ import { peerIdFromString } from '@libp2p/peer-id';
|
|
|
10
12
|
import { generateKeyPair } from '@libp2p/crypto/keys';
|
|
11
13
|
import type { ConnectionGater, PrivateKey } from '@libp2p/interface';
|
|
12
14
|
import { clusterService } from './cluster/service.js';
|
|
15
|
+
import { blockTransferService } from './cluster/block-transfer-service.js';
|
|
13
16
|
import { repoService } from './repo/service.js';
|
|
14
17
|
import { StorageRepo } from './storage/storage-repo.js';
|
|
15
18
|
import { BlockStorage } from './storage/block-storage.js';
|
|
16
19
|
import { MemoryRawStorage } from './storage/memory-storage.js';
|
|
17
20
|
import type { IRawStorage } from './storage/i-raw-storage.js';
|
|
18
|
-
import { clusterMember } from './cluster/cluster-repo.js';
|
|
21
|
+
import { clusterMember, type ReconcileBlockCallback, type CommitCertificateSink } from './cluster/cluster-repo.js';
|
|
22
|
+
import { createCommitCertStore, makeClusterCommitCertExtractor, type CommitCertStore } from './cluster/commit-cert.js';
|
|
19
23
|
import { coordinatorRepo } from './repo/coordinator-repo.js';
|
|
20
24
|
import { Libp2pKeyPeerNetwork, type NetworkMode, type NetworkStatePersistence } from './libp2p-key-network.js';
|
|
21
25
|
import { ClusterClient } from './cluster/client.js';
|
|
22
|
-
import type { IRepo, ICluster, ITransactionValidator } from '@optimystic/db-core';
|
|
26
|
+
import type { IRepo, ICluster, ITransactionValidator, BlockId, ActionRev, IBlock, IBlockChangeNotifier } from '@optimystic/db-core';
|
|
23
27
|
import type { ITransactionStateStore } from './cluster/i-transaction-state-store.js';
|
|
24
|
-
import { networkManagerService } from './network/network-manager-service.js';
|
|
28
|
+
import { networkManagerService, type NetworkManagerService } from './network/network-manager-service.js';
|
|
29
|
+
import type { SpreadOnChurnConfig, SpreadOnChurnMonitor } from './cluster/spread-on-churn.js';
|
|
25
30
|
import { fretService, Libp2pFretService } from 'p2p-fret';
|
|
26
31
|
import { syncService } from './sync/service.js';
|
|
27
32
|
import { SyncClient } from './sync/client.js';
|
|
33
|
+
import type { SyncResponse } from './sync/protocol.js';
|
|
28
34
|
import type { ClusterLatestCallback } from './repo/coordinator-repo.js';
|
|
29
35
|
import { RestorationCoordinator } from './storage/restoration-coordinator-v2.js';
|
|
30
36
|
import { RingSelector } from './storage/ring-selector.js';
|
|
31
37
|
import { StorageMonitor } from './storage/storage-monitor.js';
|
|
32
38
|
import type { StorageMonitorConfig } from './storage/storage-monitor.js';
|
|
33
39
|
import { ArachnodeFretAdapter } from './storage/arachnode-fret-adapter.js';
|
|
34
|
-
import type { RestoreCallback } from './storage/struct.js';
|
|
40
|
+
import type { RestoreCallback, BlockArchive } from './storage/struct.js';
|
|
35
41
|
import type { FretService } from 'p2p-fret';
|
|
42
|
+
import { createCohortTopicHost, type CohortTopicHostOptions } from './cohort-topic/host.js';
|
|
43
|
+
import { attachCohortChangeBridge } from './cohort-topic/change-bridge.js';
|
|
44
|
+
import { createReactivitySelfMembershipGate, reactivityTailBytes } from './cohort-topic/reactivity-membership-gate.js';
|
|
45
|
+
import { Libp2pReactivityNotifyTransport, registerNotifyHandler } from './reactivity/notify-transport.js';
|
|
46
|
+
import {
|
|
47
|
+
Libp2pReactivityRecoverTransport,
|
|
48
|
+
createLibp2pRecoverDialer,
|
|
49
|
+
registerRecoverHandler,
|
|
50
|
+
createRecoverRequestSigners,
|
|
51
|
+
} from './reactivity/recover-transport.js';
|
|
52
|
+
import { ReactivityForwarderHost, reactivityDirectSubscribers, reactivityNotificationTopicId } from './reactivity/forwarder-host.js';
|
|
53
|
+
import { ReactivityOriginationManager } from './reactivity/origination-manager.js';
|
|
54
|
+
import { ReactivityPushStateGossipDriver, registerPushStateGossipHandler, type ReactivityGossipCollection } from './reactivity/push-state-gossip.js';
|
|
55
|
+
import { RotationReRegistrationScheduler } from './reactivity/rotation-rereg-scheduler.js';
|
|
56
|
+
import { ReactivitySubscriberRegistry } from './reactivity/subscriber-registry.js';
|
|
57
|
+
import { DEFAULT_REACTIVITY_PROTOCOLS, reactivityProtocolList } from './reactivity/protocols.js';
|
|
58
|
+
import { registerMatchmakingQueryHandler } from './matchmaking/query-transport.js';
|
|
59
|
+
import { DEFAULT_MATCHMAKING_PROTOCOLS, matchmakingProtocolList } from './matchmaking/protocols.js';
|
|
60
|
+
import { signPeer } from './cohort-topic/peer-sig.js';
|
|
61
|
+
import {
|
|
62
|
+
createNotificationVerifier,
|
|
63
|
+
createCorrelationReplayGuard,
|
|
64
|
+
createStickyCohortHintCache,
|
|
65
|
+
reactivityNodePolicy,
|
|
66
|
+
createTierAddressing,
|
|
67
|
+
createRingHash,
|
|
68
|
+
Tier,
|
|
69
|
+
b64urlToBytes,
|
|
70
|
+
bytesToB64url,
|
|
71
|
+
type NotificationV1,
|
|
72
|
+
type CohortRef,
|
|
73
|
+
type PushStateGossipV1,
|
|
74
|
+
type PushStateInit,
|
|
75
|
+
type NotificationVerifier,
|
|
76
|
+
} from '@optimystic/db-core';
|
|
36
77
|
import { PartitionDetector } from './cluster/partition-detector.js';
|
|
78
|
+
import { createLogger } from './logger.js';
|
|
37
79
|
import { PeerReputationService } from './reputation/peer-reputation.js';
|
|
38
80
|
import { DisputeService } from './dispute/dispute-service.js';
|
|
39
81
|
import { DisputeClient } from './dispute/client.js';
|
|
@@ -42,6 +84,9 @@ import type { DisputeConfig } from './dispute/types.js';
|
|
|
42
84
|
type Libp2pInit = NonNullable<Parameters<typeof createLibp2p>[0]>;
|
|
43
85
|
export type Libp2pTransports = NonNullable<Libp2pInit['transports']>;
|
|
44
86
|
|
|
87
|
+
/** Logger for the reactivity node-wiring (origination/forwarder/recover/rotation composition). */
|
|
88
|
+
const reactivityWiringLog = createLogger('reactivity-node-wiring');
|
|
89
|
+
|
|
45
90
|
/** Factory function or instance for creating raw storage */
|
|
46
91
|
export type RawStorageProvider = IRawStorage | (() => IRawStorage);
|
|
47
92
|
|
|
@@ -99,9 +144,12 @@ export type NodeOptions = {
|
|
|
99
144
|
/**
|
|
100
145
|
* Responsibility K - the replica set size for determining cluster membership.
|
|
101
146
|
* This is distinct from kBucketSize (DHT routing) and clusterSize (consensus quorum).
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
147
|
+
* On the repo path, a node checks whether it is in the top responsibilityK peers
|
|
148
|
+
* (by XOR distance) for the key and redirects to closer peers if not. On the cluster
|
|
149
|
+
* update path it is a small-mesh bypass threshold: when the record's peer set is
|
|
150
|
+
* smaller than this, the update is processed locally regardless of membership;
|
|
151
|
+
* otherwise a non-member redirects to the responsible peers.
|
|
152
|
+
* Default: 1 (only the closest/member peer is responsible)
|
|
105
153
|
*/
|
|
106
154
|
responsibilityK?: number;
|
|
107
155
|
|
|
@@ -111,6 +159,12 @@ export type NodeOptions = {
|
|
|
111
159
|
storage?: StorageMonitorConfig;
|
|
112
160
|
};
|
|
113
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Churn-resilient spread protocol tuning. Absent -> enabled with defaults
|
|
164
|
+
* (see SpreadOnChurnConfig). Set { enabled: false } to disable spread on this node.
|
|
165
|
+
*/
|
|
166
|
+
spreadOnChurn?: Partial<SpreadOnChurnConfig>;
|
|
167
|
+
|
|
114
168
|
/** Transaction validator for cluster consensus */
|
|
115
169
|
validator?: ITransactionValidator;
|
|
116
170
|
|
|
@@ -123,6 +177,39 @@ export type NodeOptions = {
|
|
|
123
177
|
/** Optional persistent store for 2PC transaction state (enables crash recovery) */
|
|
124
178
|
transactionStateStore?: ITransactionStateStore;
|
|
125
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Optional sink for the consensus commit certificate, fired per committed action just before the
|
|
182
|
+
* commit is applied to local storage (see {@link CommitCertificateSink}). This is the cluster-side
|
|
183
|
+
* half of the reactivity origination path: a caller wiring reactivity supplies a
|
|
184
|
+
* {@link CommitCertStore}'s `put` here, then resolves it via {@link makeClusterCommitCertExtractor}
|
|
185
|
+
* when it installs the change-notifier bridge ({@link attachCohortChangeBridge}) on the running
|
|
186
|
+
* node. Absent → zero cost (no cert is assembled).
|
|
187
|
+
*/
|
|
188
|
+
onCommitCertificate?: CommitCertificateSink;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Opt-in cohort-topic substrate activation (reactivity / matchmaking origination). Default OFF →
|
|
192
|
+
* the node keeps today's bare `blockChangeNotifier = storageRepo` behavior at zero cohort cost (no
|
|
193
|
+
* host, no cert store; a caller-supplied {@link onCommitCertificate} is the only sink). When
|
|
194
|
+
* `enabled`, the node-base constructs the cohort-topic host post-assembly, builds a real FRET-backed
|
|
195
|
+
* `selfIsCohortMember` gate over `coord_0(H(tailId ‖ "reactivity"))`, and installs the change-notifier
|
|
196
|
+
* origination bridge — making reactivity origination live for ALL collections created on the node.
|
|
197
|
+
*
|
|
198
|
+
* A failure to construct the host (or a missing FRET service) **hard-fails** node startup: the
|
|
199
|
+
* operator opted in, so silently degrading to the bare notifier would hide misconfiguration.
|
|
200
|
+
*/
|
|
201
|
+
cohortTopic?: {
|
|
202
|
+
/** Master switch. Absent/`false` → dormant, zero cost. */
|
|
203
|
+
enabled: boolean;
|
|
204
|
+
/**
|
|
205
|
+
* Requested cohort size; MUST match the host's `wantK` so the membership gate checks the same
|
|
206
|
+
* cohort the host serves. Default 16 (the host's default).
|
|
207
|
+
*/
|
|
208
|
+
wantK?: number;
|
|
209
|
+
/** Optional pass-through host tuning (profile / minSigs / fanout / gossipIntervalMs / antiDos / promotion). */
|
|
210
|
+
host?: Omit<CohortTopicHostOptions, 'privateKey' | 'wantK'>;
|
|
211
|
+
};
|
|
212
|
+
|
|
126
213
|
/**
|
|
127
214
|
* Optional Ed25519 private key for this node. When provided, the libp2p
|
|
128
215
|
* node uses this identity instead of generating a fresh keypair. Use this
|
|
@@ -149,6 +236,39 @@ function resolveStorage(provider: RawStorageProvider | undefined): IRawStorage {
|
|
|
149
236
|
return typeof provider === 'function' ? provider() : provider;
|
|
150
237
|
}
|
|
151
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Resolve the full FRET engine the cohort-topic host needs.
|
|
241
|
+
*
|
|
242
|
+
* `createCohortTopicHost` consumes the complete {@link FretService} engine surface — notably
|
|
243
|
+
* `setActivityHandler` (and `routeAct`, size estimation, …). The value at `node.services.fret` is the
|
|
244
|
+
* libp2p `Libp2pFretService` *wrapper*, which re-exports only a subset (`assembleCohort`, `routeAct`, …)
|
|
245
|
+
* and keeps the real engine private behind its lazy `ensure()` accessor. By the time activation runs the
|
|
246
|
+
* engine is already initialized — the wrapper's `Startable.start()` ran during `node.start()` — and the
|
|
247
|
+
* engine and wrapper share one underlying routing store, so the host and the membership gate observe the
|
|
248
|
+
* same cohort state. Returns the engine when reachable; otherwise the value as-is (a test may inject a
|
|
249
|
+
* raw engine that needs no unwrapping).
|
|
250
|
+
*/
|
|
251
|
+
function resolveFretEngine(fret: FretService | undefined): FretService | undefined {
|
|
252
|
+
if (!fret) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
const candidate = fret as unknown as { ensure?: () => FretService };
|
|
256
|
+
return typeof candidate.ensure === 'function' ? candidate.ensure() : fret;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The raw topic id bytes of a collection's current served reactivity {@link PushState}, or `undefined` if the
|
|
261
|
+
* node serves none. The forwarder host keys its served map by topicId, but a **backfill** recover request
|
|
262
|
+
* carries only a collectionId — so the drain-redirect binding resolves the collection's current tail topic
|
|
263
|
+
* here (the highest-`lastRevision` served PushState) before consulting `rotationRedirectFor`. While the old
|
|
264
|
+
* tail is the only served state this resolves it (and its drain gate redirects); once the new tail is served
|
|
265
|
+
* this resolves the new tail (no gate → no redirect), exactly as the recover serve's backfill path intends.
|
|
266
|
+
*/
|
|
267
|
+
function resolveCurrentServedTopic(forwarderHost: ReactivityForwarderHost, collectionId: string): Uint8Array | undefined {
|
|
268
|
+
const ps = forwarderHost.pushStateForCollection(collectionId);
|
|
269
|
+
return ps === undefined ? undefined : b64urlToBytes(ps.topicId);
|
|
270
|
+
}
|
|
271
|
+
|
|
152
272
|
export async function createLibp2pNodeBase(
|
|
153
273
|
options: NodeOptions,
|
|
154
274
|
defaults: {
|
|
@@ -204,6 +324,23 @@ export async function createLibp2pNodeBase(
|
|
|
204
324
|
const listenAddrs = options.listenAddrs ?? defaults.listenAddrs;
|
|
205
325
|
const transports = options.transports ?? defaults.transports;
|
|
206
326
|
|
|
327
|
+
// --- cohort-topic substrate activation (opt-in; default off → today's bare behavior, zero cost) ---
|
|
328
|
+
const cohortEnabled = options.cohortTopic?.enabled === true;
|
|
329
|
+
// Resolve wantK ONCE so the post-assembly host serves and the membership gate checks the SAME cohort.
|
|
330
|
+
const cohortWantK = options.cohortTopic?.wantK ?? 16;
|
|
331
|
+
// When enabled, the cluster member records the consensus commit cert into this store synchronously,
|
|
332
|
+
// BEFORE `storageRepo.commit` emits the change event the bridge's extractor resolves it from (see
|
|
333
|
+
// cluster-repo.ts §applyConsensusOperation). Created early because the sink must be passed into
|
|
334
|
+
// `clusterMember(...)` below. Composed with any caller-supplied `onCommitCertificate` so both fire.
|
|
335
|
+
const certStore: CommitCertStore | undefined = cohortEnabled ? createCommitCertStore() : undefined;
|
|
336
|
+
const onCommitCertificate: CommitCertificateSink | undefined = certStore
|
|
337
|
+
// `certStore.put` runs FIRST so origination's cert capture cannot be defeated by a throwing caller
|
|
338
|
+
// sink: the whole composed call is isolated in `ClusterMember.captureCommitCert`, so a caller sink
|
|
339
|
+
// that threw before the store was written would make that commit silently never originate. Ordering
|
|
340
|
+
// the store first keeps origination correct regardless of the caller sink (`put` never throws).
|
|
341
|
+
? (actionId, cert): void => { certStore.put(actionId, cert); options.onCommitCertificate?.(actionId, cert); }
|
|
342
|
+
: options.onCommitCertificate;
|
|
343
|
+
|
|
207
344
|
const libp2pOptions: unknown = {
|
|
208
345
|
start: false,
|
|
209
346
|
privateKey: nodePrivateKey,
|
|
@@ -226,6 +363,13 @@ export async function createLibp2pNodeBase(
|
|
|
226
363
|
protocolPrefix: `/optimystic/${options.networkName}`
|
|
227
364
|
}),
|
|
228
365
|
ping: ping(),
|
|
366
|
+
// DCUtR (hole-punch) upgrades relayed node↔node connections to direct
|
|
367
|
+
// ones; AutoNAT learns this node's public reachability via peer dial-back.
|
|
368
|
+
// Both are always-on and depend on `identify` above. They are inert where
|
|
369
|
+
// the transport can't hole-punch or dial back (e.g. browser/WS-only), which
|
|
370
|
+
// is acceptable — they neither throw nor break the build in that case.
|
|
371
|
+
dcutr: dcutr(),
|
|
372
|
+
autoNAT: autoNAT(),
|
|
229
373
|
pubsub: gossipsub({
|
|
230
374
|
allowPublishToZeroTopicPeers: true,
|
|
231
375
|
heartbeatInterval: 7000
|
|
@@ -237,15 +381,26 @@ export async function createLibp2pNodeBase(
|
|
|
237
381
|
cluster: (components: any) => {
|
|
238
382
|
const serviceFactory = clusterService({
|
|
239
383
|
protocolPrefix: `/optimystic/${options.networkName}`,
|
|
240
|
-
configuredClusterSize: options.clusterSize ?? 10,
|
|
241
|
-
allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
|
|
242
|
-
clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5,
|
|
243
384
|
responsibilityK: options.responsibilityK ?? 1
|
|
244
385
|
});
|
|
245
386
|
return serviceFactory({
|
|
246
387
|
logger: components.logger,
|
|
247
388
|
registrar: components.registrar,
|
|
248
|
-
cluster: clusterProxy
|
|
389
|
+
cluster: clusterProxy,
|
|
390
|
+
// Identity for membership scoping on the update path. peerId is a core
|
|
391
|
+
// libp2p component, available at service-construction time.
|
|
392
|
+
peerId: components.peerId,
|
|
393
|
+
// Fallback addr resolver for redirect targets whose multiaddrs are not
|
|
394
|
+
// already embedded in record.peers.
|
|
395
|
+
getConnectionAddrs: (peerId: any) => {
|
|
396
|
+
const conns = components.libp2p?.getConnections?.(peerId) ?? [];
|
|
397
|
+
const addrs: string[] = [];
|
|
398
|
+
for (const c of conns) {
|
|
399
|
+
const addr = c.remoteAddr?.toString?.();
|
|
400
|
+
if (addr) addrs.push(addr);
|
|
401
|
+
}
|
|
402
|
+
return addrs;
|
|
403
|
+
}
|
|
249
404
|
});
|
|
250
405
|
},
|
|
251
406
|
|
|
@@ -254,6 +409,15 @@ export async function createLibp2pNodeBase(
|
|
|
254
409
|
protocolPrefix: `/optimystic/${options.networkName}`,
|
|
255
410
|
responsibilityK: options.responsibilityK ?? 1
|
|
256
411
|
});
|
|
412
|
+
// RepoService.checkRedirect needs the running node (network manager for the
|
|
413
|
+
// responsible-set computation, self id for the membership check, connection
|
|
414
|
+
// addrs for redirect targets). The libp2p components.libp2p proxy does NOT
|
|
415
|
+
// reliably resolve from inside a service at request time, so the node is
|
|
416
|
+
// injected explicitly post-construction via setLibp2p(node) below — the same
|
|
417
|
+
// mechanism networkManager/fret use — rather than forwarded here. checkRedirect
|
|
418
|
+
// keys the responsible set on the RAW encoded block id
|
|
419
|
+
// (getCluster(encode(blockKey)) → hashKey(encode(...))), matching the
|
|
420
|
+
// coordinator's findCluster(encode(blockId)) — same cohort, no spurious redirect.
|
|
257
421
|
return serviceFactory({
|
|
258
422
|
logger: components.logger,
|
|
259
423
|
registrar: components.registrar,
|
|
@@ -272,6 +436,19 @@ export async function createLibp2pNodeBase(
|
|
|
272
436
|
});
|
|
273
437
|
},
|
|
274
438
|
|
|
439
|
+
// Block-transfer protocol handler for churn re-replication. Wired to the
|
|
440
|
+
// *local* storageRepo (not repoProxy): a pushed replica must land in this
|
|
441
|
+
// node's own storage, not be re-routed through the cluster-coordinated repo.
|
|
442
|
+
blockTransfer: (components: any) => {
|
|
443
|
+
const serviceFactory = blockTransferService({
|
|
444
|
+
protocolPrefix: `/optimystic/${options.networkName}`
|
|
445
|
+
});
|
|
446
|
+
return serviceFactory({
|
|
447
|
+
registrar: components.registrar,
|
|
448
|
+
repo: storageRepo
|
|
449
|
+
});
|
|
450
|
+
},
|
|
451
|
+
|
|
275
452
|
networkManager: (components: any) => {
|
|
276
453
|
const svcFactory = networkManagerService({
|
|
277
454
|
clusterSize: options.clusterSize ?? 10,
|
|
@@ -308,6 +485,11 @@ export async function createLibp2pNodeBase(
|
|
|
308
485
|
// Inject libp2p reference into services that need it before start
|
|
309
486
|
try { ((node as any).services?.fret as any)?.setLibp2p?.(node); } catch { }
|
|
310
487
|
try { ((node as any).services?.networkManager as any)?.setLibp2p?.(node); } catch { }
|
|
488
|
+
// RepoService.checkRedirect resolves the network manager / self id / connection
|
|
489
|
+
// addrs through this injected node (the components.libp2p proxy is unreliable
|
|
490
|
+
// from inside a service at request time). Done before start() so the protocol
|
|
491
|
+
// handler is live with a resolvable node from its first request.
|
|
492
|
+
try { ((node as any).services?.repo as any)?.setLibp2p?.(node); } catch { }
|
|
311
493
|
|
|
312
494
|
await node.start();
|
|
313
495
|
|
|
@@ -337,6 +519,60 @@ export async function createLibp2pNodeBase(
|
|
|
337
519
|
partitionDetectionWindow: 60000
|
|
338
520
|
};
|
|
339
521
|
|
|
522
|
+
// Fetch a block archive from one cohort peer over the sync protocol, bounded by a
|
|
523
|
+
// per-peer timeout so an unreachable peer can't stall reconciliation. Mirrors the
|
|
524
|
+
// SyncClient query in `clusterLatestCallback`, but returns the full archive (which
|
|
525
|
+
// carries the materialized block) rather than only the latest ActionRev.
|
|
526
|
+
const fetchArchiveFromPeer = async (peerIdStr: string, blockId: BlockId): Promise<BlockArchive | undefined> => {
|
|
527
|
+
let peerId: ReturnType<typeof peerIdFromString>;
|
|
528
|
+
try {
|
|
529
|
+
peerId = peerIdFromString(peerIdStr);
|
|
530
|
+
} catch {
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
if (peerId.equals(node.peerId)) return undefined;
|
|
534
|
+
const syncClient = new SyncClient(peerId, keyNetwork, protocolPrefix);
|
|
535
|
+
try {
|
|
536
|
+
const response = await Promise.race<SyncResponse>([
|
|
537
|
+
syncClient.requestBlock({ blockId, rev: undefined }),
|
|
538
|
+
new Promise<SyncResponse>(resolve => { setTimeout(() => resolve({ success: false }), 1000).unref(); })
|
|
539
|
+
]);
|
|
540
|
+
return response.success ? response.archive : undefined;
|
|
541
|
+
} catch {
|
|
542
|
+
// Peer unreachable / no data — caller falls back to the next cohort peer.
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// Active reconciliation for a block this member committed without the matching pend
|
|
548
|
+
// (cohort drift). Queries the commit cohort (self already excluded) for the block,
|
|
549
|
+
// picks the highest revision that is at least the committed rev, and persists it via
|
|
550
|
+
// the churn-replication funnel so the block is no longer under-replicated.
|
|
551
|
+
const reconcileBlock: ReconcileBlockCallback = async (blockId, committed, cohortPeerIds) => {
|
|
552
|
+
const targets = cohortPeerIds.filter(id => id !== node.peerId.toString());
|
|
553
|
+
if (targets.length === 0) return;
|
|
554
|
+
|
|
555
|
+
const archives = await Promise.all(targets.map(peerIdStr => fetchArchiveFromPeer(peerIdStr, blockId)));
|
|
556
|
+
|
|
557
|
+
let best: { block: IBlock; source: ActionRev } | undefined;
|
|
558
|
+
for (const archive of archives) {
|
|
559
|
+
if (!archive) continue;
|
|
560
|
+
const revs = Object.keys(archive.revisions).map(Number);
|
|
561
|
+
if (revs.length === 0) continue;
|
|
562
|
+
const maxRev = Math.max(...revs);
|
|
563
|
+
if (maxRev < committed.rev) continue;
|
|
564
|
+
const data = archive.revisions[maxRev];
|
|
565
|
+
if (!data?.block) continue;
|
|
566
|
+
if (!best || maxRev > best.source.rev) {
|
|
567
|
+
best = { block: data.block, source: { actionId: data.action.actionId, rev: maxRev } };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (best) {
|
|
572
|
+
await storageRepo.saveReplicatedBlock(blockId, best.block, best.source);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
340
576
|
clusterImpl = clusterMember({
|
|
341
577
|
storageRepo,
|
|
342
578
|
peerNetwork: keyNetwork,
|
|
@@ -348,7 +584,15 @@ export async function createLibp2pNodeBase(
|
|
|
348
584
|
validator: options.validator,
|
|
349
585
|
reputation,
|
|
350
586
|
consensusConfig,
|
|
351
|
-
stateStore: options.transactionStateStore
|
|
587
|
+
stateStore: options.transactionStateStore,
|
|
588
|
+
reconcileBlock,
|
|
589
|
+
onCommitCertificate
|
|
590
|
+
// `recomputeArbitratorSet` (invalidation layer-2) is intentionally NOT wired here yet: a live FRET
|
|
591
|
+
// recompute needs a churn-tolerance window so it does not false-reject legitimate certificates from
|
|
592
|
+
// late-joiners (a liveness regression). Until that is tuned against live topology — and the
|
|
593
|
+
// cohort-topic membership-cert trust anchor (layer 3) lands — invalidation verification runs on the
|
|
594
|
+
// challenger-bound set + membership + dedup (layer 1) and LOGS the residual anchoring gap. See
|
|
595
|
+
// `verifyInvalidationCertificate` and `tickets/plan/cohort-topic-membership-cert-trust-anchoring.md`.
|
|
352
596
|
});
|
|
353
597
|
|
|
354
598
|
const coordinatorRepoFactory = coordinatorRepo(
|
|
@@ -409,6 +653,65 @@ export async function createLibp2pNodeBase(
|
|
|
409
653
|
await (coordinatedRepo as import('./repo/coordinator-repo.js').CoordinatorRepo).recoverTransactions();
|
|
410
654
|
}
|
|
411
655
|
|
|
656
|
+
// --- Churn-resilient spread: drive SpreadOnChurnMonitor on a live node ---
|
|
657
|
+
// Nothing previously activated the SENDING side of the churn-resilient spread protocol on a
|
|
658
|
+
// real node. Here we init + start the monitor and feed it the blocks this node physically
|
|
659
|
+
// holds, so a debounced connection:close re-pushes them to expansion-cohort peers (the
|
|
660
|
+
// receiver durably persists each push via saveReplicatedBlock).
|
|
661
|
+
const networkManager = (node as any).services?.networkManager as NetworkManagerService | undefined;
|
|
662
|
+
let spreadMonitor: SpreadOnChurnMonitor | undefined;
|
|
663
|
+
let offOwnedBlockFeed: (() => void) | undefined;
|
|
664
|
+
if (networkManager && (options.spreadOnChurn?.enabled ?? true) !== false) {
|
|
665
|
+
try {
|
|
666
|
+
spreadMonitor = networkManager.initSpreadOnChurnMonitor(
|
|
667
|
+
partitionDetector,
|
|
668
|
+
storageRepo,
|
|
669
|
+
keyNetwork,
|
|
670
|
+
options.clusterSize ?? 10,
|
|
671
|
+
protocolPrefix,
|
|
672
|
+
options.spreadOnChurn,
|
|
673
|
+
);
|
|
674
|
+
await spreadMonitor.start();
|
|
675
|
+
// Feed owned blocks: every block this node commits OR receives as a replica fires
|
|
676
|
+
// storageRepo.onAnyCollectionChange. Subscribe to storageRepo DIRECTLY (not
|
|
677
|
+
// node.blockChangeNotifier): the cohort-topic activation block below may replace
|
|
678
|
+
// blockChangeNotifier with a decorating bridge, but storageRepo keeps emitting on its
|
|
679
|
+
// own surface regardless of that opt-in. NOTE: blocks already durable from a previous
|
|
680
|
+
// run are NOT re-emitted on startup, so they are not tracked until next touched -
|
|
681
|
+
// acceptable here (churn re-replication re-derives over time); an initial-scan is a
|
|
682
|
+
// follow-on enhancement.
|
|
683
|
+
const monitor = spreadMonitor;
|
|
684
|
+
offOwnedBlockFeed = storageRepo.onAnyCollectionChange((e) => {
|
|
685
|
+
for (const blockId of e.blockIds) monitor.trackBlock(blockId);
|
|
686
|
+
});
|
|
687
|
+
} catch (err) {
|
|
688
|
+
// Spread is a resilience optimization, not a correctness requirement - a wiring
|
|
689
|
+
// failure (e.g. FRET briefly unavailable) must NOT hard-fail node startup, unlike the
|
|
690
|
+
// operator-opted-in cohortTopic block. Log and continue with spread inert.
|
|
691
|
+
((node as any).logger?.forComponent?.('db-p2p:spread-on-churn'))?.('init failed: %o', err);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Expose for tests/diagnostics (mirrors node.keyNetwork / node.reputation).
|
|
696
|
+
(node as any).spreadOnChurnMonitor = spreadMonitor;
|
|
697
|
+
|
|
698
|
+
// Disposal: release the owned-block subscription + stop the monitor deterministically
|
|
699
|
+
// before the transports close. Composes with the arachnode / clusterMember / cohort-topic
|
|
700
|
+
// stop wrappers (each calls its captured previousStop last). Both steps are idempotent (the
|
|
701
|
+
// unsubscribe is flag-guarded; SpreadOnChurnMonitor.stop early-returns when not running), so
|
|
702
|
+
// a double node.stop() does not throw.
|
|
703
|
+
{
|
|
704
|
+
const previousStop = node.stop.bind(node);
|
|
705
|
+
node.stop = async () => {
|
|
706
|
+
try {
|
|
707
|
+
offOwnedBlockFeed?.();
|
|
708
|
+
if (spreadMonitor) await spreadMonitor.stop();
|
|
709
|
+
} finally {
|
|
710
|
+
await previousStop();
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
412
715
|
// Initialize Arachnode ring membership and restoration
|
|
413
716
|
const enableArachnode = options.arachnode?.enableRingZulu ?? true;
|
|
414
717
|
if (enableArachnode) {
|
|
@@ -519,9 +822,314 @@ export async function createLibp2pNodeBase(
|
|
|
519
822
|
// Expose coordinated repo and storage for external use
|
|
520
823
|
(node as any).coordinatedRepo = coordinatedRepo;
|
|
521
824
|
(node as any).storageRepo = storageRepo;
|
|
825
|
+
// The StorageRepo is the single commit funnel for both the coordinated and
|
|
826
|
+
// direct paths, so it is the node's per-collection change-notifier origin. This is the
|
|
827
|
+
// default; the cohort-topic activation block below REPLACES it with the origination-decorating
|
|
828
|
+
// bridge notifier when the substrate is enabled.
|
|
829
|
+
(node as any).blockChangeNotifier = storageRepo;
|
|
522
830
|
(node as any).keyNetwork = keyNetwork;
|
|
523
831
|
(node as any).reputation = reputation;
|
|
524
832
|
(node as any).disputeService = disputeServiceInstance;
|
|
525
833
|
|
|
834
|
+
// --- Cohort-topic origination activation (post-node: consumes the fully-assembled node + FRET) ---
|
|
835
|
+
// This is the only place that is after the node + FRET are assembled (node.start() done, fretSvc
|
|
836
|
+
// available) yet before any caller can capture `blockChangeNotifier` — the Quereus collection-factory
|
|
837
|
+
// captures it once, immediately after createLibp2pNode returns, and reuses that reference as
|
|
838
|
+
// `localChangeNotifier` for every NetworkTransactor it builds. Installing the bridge here makes the
|
|
839
|
+
// origination path live for ALL collections created on the node.
|
|
840
|
+
if (cohortEnabled) {
|
|
841
|
+
// The host needs the full FRET engine surface; node.services.fret is the wrapper (see resolveFretEngine).
|
|
842
|
+
const fret = resolveFretEngine(fretSvc);
|
|
843
|
+
if (!fret) {
|
|
844
|
+
// Operator opted in; degrading silently to the bare notifier would hide misconfiguration.
|
|
845
|
+
// The node has already started (transports open, FRET running), so tear it down before the
|
|
846
|
+
// hard-fail rather than leaking a started node + open transports on the rejection.
|
|
847
|
+
await node.stop();
|
|
848
|
+
throw new Error('cohortTopic enabled but the FRET service is unavailable on the node');
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// A host-construction failure also hard-fails (operator opted in); stop the started node first so
|
|
852
|
+
// the rejection does not leak open transports / a running FRET service. node.stop() runs the
|
|
853
|
+
// already-installed arachnode + clusterMember teardown wrappers and closes the node's connections.
|
|
854
|
+
let host: Awaited<ReturnType<typeof createCohortTopicHost>>;
|
|
855
|
+
try {
|
|
856
|
+
host = await createCohortTopicHost(node, fret, {
|
|
857
|
+
...(options.cohortTopic!.host ?? {}),
|
|
858
|
+
// Wire the node's reputation service in as the production backing for the bootstrap-evidence
|
|
859
|
+
// referee verifier (the `{ isBanned, getScore }` view `PeerReputationService` satisfies), so a
|
|
860
|
+
// configured cohort genuinely gates cold-root `bootstrap: true` (PoW always; reputation when a
|
|
861
|
+
// referee endorsement is offered; a signed reference to an existing parent topic on any tier).
|
|
862
|
+
// The node service is the *default* backing — a caller that supplies its own `antiDos.reputation`
|
|
863
|
+
// (or any other `antiDos` override) still wins, since the caller spread comes last.
|
|
864
|
+
antiDos: { reputation, ...(options.cohortTopic!.host?.antiDos) },
|
|
865
|
+
// committedParentTopicReader (the T0/T1 committed-tier parent-reference existence backing) is
|
|
866
|
+
// intentionally left unwired: no coord-keyed committed-membership index exists yet (the
|
|
867
|
+
// transaction-log commit certificate is keyed by action, not by coord_0). So the host default
|
|
868
|
+
// fails T0/T1 parent-ref existence closed — a FRET-cached cert must not vouch for committed-tier
|
|
869
|
+
// existence (committed-tier integrity) — while T2/T3 parent-ref consults the FRET membership cache
|
|
870
|
+
// for real. The dedicated committed backing is the follow-on `cohort-topic-parent-ref-tx-log-content`;
|
|
871
|
+
// an operator may still pass one via cohortTopic.host.committedParentTopicReader.
|
|
872
|
+
privateKey: nodePrivateKey, // real k − x threshold signing
|
|
873
|
+
wantK: cohortWantK,
|
|
874
|
+
});
|
|
875
|
+
} catch (err) {
|
|
876
|
+
await node.stop();
|
|
877
|
+
throw err;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// selfIsCohortMember: this node owns the collection's reactivity-topic fan-out iff it is in the
|
|
881
|
+
// FRET cohort around coord_0(H(currentTailId ‖ "reactivity")). Uses db-core's default hashes
|
|
882
|
+
// (createReactivityTopicAnchor / createTierAddressing / createRingHash), byte-identical to the
|
|
883
|
+
// host's internal `new RingHash()` and the subscriber-side anchor, and the SAME cohortWantK as
|
|
884
|
+
// the host — so the coord + cohort line up across origination and subscription.
|
|
885
|
+
const selfIsCohortMember = createReactivitySelfMembershipGate({
|
|
886
|
+
fret,
|
|
887
|
+
selfPeerId: node.peerId.toString(),
|
|
888
|
+
wantK: cohortWantK,
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const { unsubscribe } = attachCohortChangeBridge(
|
|
892
|
+
node as unknown as { blockChangeNotifier?: IBlockChangeNotifier },
|
|
893
|
+
{
|
|
894
|
+
source: storageRepo,
|
|
895
|
+
service: host.service,
|
|
896
|
+
selfIsCohortMember,
|
|
897
|
+
extractCommitCert: makeClusterCommitCertExtractor(certStore!),
|
|
898
|
+
},
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
// Expose the host so the reactivity origination wiring (and the activation test) can install
|
|
902
|
+
// `CohortTopicService.onLocalCommit`.
|
|
903
|
+
(node as any).cohortTopicHost = host;
|
|
904
|
+
|
|
905
|
+
// --- Reactivity notification transport (origination → fan-out → inbound delivery → push-state gossip) ---
|
|
906
|
+
// Compose notify + forwarder-host + push-state-gossip onto the cohort-topic host so a committed change
|
|
907
|
+
// on a tail-cohort member actually reaches subscribers on OTHER nodes over real sockets. The change
|
|
908
|
+
// bridge above fires `onLocalCommit`; this is what the emitted notifications travel over.
|
|
909
|
+
// (docs/reactivity.md §Notification origination / §Propagation.) Reactivity reuses the canonical,
|
|
910
|
+
// network-agnostic protocol IDs, matching the cohort-topic family's production default.
|
|
911
|
+
const selfPeerId = node.peerId.toString();
|
|
912
|
+
const reactivityProtocols = DEFAULT_REACTIVITY_PROTOCOLS;
|
|
913
|
+
const reactivityProfile = host.profile; // Edge ⇒ subscriber-only via the policy gate; Core forwards.
|
|
914
|
+
const reactivityPolicy = reactivityNodePolicy(reactivityProfile);
|
|
915
|
+
// db-core default anchor + tier addressing, byte-identical to the host's `new RingHash()`, the
|
|
916
|
+
// origination gate, and the subscriber-side anchor — so coord_0 derivation lines up everywhere.
|
|
917
|
+
const reactivityAddressing = createTierAddressing(createRingHash());
|
|
918
|
+
// Reactivity's forwarder cohort sits at coord_0 — TREE tier 0 (peer-independent), distinct from the
|
|
919
|
+
// CAPACITY tier T3 the verifier/willingness use. `registry.findServing` keys on the engine's tree
|
|
920
|
+
// depth, so the served reactivity engine is found at tree tier 0, never at 3.
|
|
921
|
+
const REACTIVITY_FORWARDER_TREE_TIER = 0;
|
|
922
|
+
|
|
923
|
+
// Node-level subscriber registry: a constructed ReactivitySubscriptionManager registers here so a
|
|
924
|
+
// socket-delivered NotificationV1 reaches it. (The Quereus Database.watch → manager bridge that
|
|
925
|
+
// CONSTRUCTS managers stays the backlog item optimystic-network-reactive-watch-integration-test.)
|
|
926
|
+
const reactivitySubscribers = new ReactivitySubscriberRegistry();
|
|
927
|
+
(node as any).reactivitySubscribers = reactivitySubscribers;
|
|
928
|
+
|
|
929
|
+
// 1. Notify transport — unicast NotificationV1 send + inbound subscribe. selfPeerId guards self-dials.
|
|
930
|
+
const notify = new Libp2pReactivityNotifyTransport(node, { selfPeerId });
|
|
931
|
+
|
|
932
|
+
// 2. Forwarder host — turns the forward decision into live fan-out over the notify transport.
|
|
933
|
+
const forwarderHost = new ReactivityForwarderHost({
|
|
934
|
+
transport: notify,
|
|
935
|
+
selfPeerId,
|
|
936
|
+
profile: reactivityProfile,
|
|
937
|
+
pushStateInit: (topicId: Uint8Array, n: NotificationV1): PushStateInit => ({
|
|
938
|
+
collectionId: n.collectionId,
|
|
939
|
+
topicId: bytesToB64url(topicId),
|
|
940
|
+
tailIdAtJoin: n.tailId,
|
|
941
|
+
deltaMaxBytes: reactivityPolicy.deltaMaxBytes,
|
|
942
|
+
}),
|
|
943
|
+
verifierFor: (): NotificationVerifier => createNotificationVerifier({ verifier: host.service.verifier(), tier: Tier.T3 }),
|
|
944
|
+
directSubscribers: (topicId: Uint8Array): string[] => {
|
|
945
|
+
// Find the served reactivity engine at TREE tier 0 (see REACTIVITY_FORWARDER_TREE_TIER) and read
|
|
946
|
+
// its direct-subscriber records. The adapter filters to reactivity appState and maps participantId
|
|
947
|
+
// bytes → dialable peer-id strings (the transport's `peerIdFromString` space) — NOT base64url,
|
|
948
|
+
// which would silently fail to dial. `undefined` (no subscriber has registered here yet) ⇒ [].
|
|
949
|
+
const engine = host.registry.findServing(topicId, REACTIVITY_FORWARDER_TREE_TIER);
|
|
950
|
+
return engine === undefined ? [] : reactivityDirectSubscribers(engine, topicId);
|
|
951
|
+
},
|
|
952
|
+
// No childCohorts until cohort-topic-parent-child-link populates PushState.childCohorts (single
|
|
953
|
+
// tier-0 reach today); wire the resolver anyway. A child cohort's primary is the FRET-nearest member
|
|
954
|
+
// of its coord, returned as a peer-id string (the dial space).
|
|
955
|
+
resolveChildPrimary: (ref: CohortRef): string | undefined => {
|
|
956
|
+
const peers = fret.assembleCohort(b64urlToBytes(ref.coord), cohortWantK);
|
|
957
|
+
return peers.length > 0 ? peers[0] : undefined;
|
|
958
|
+
},
|
|
959
|
+
deliverLocal: (topicId: Uint8Array, n: NotificationV1): void => reactivitySubscribers.deliver(topicId, n),
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
// Inbound notify frames → forwarder host (subscriber role delivers in-process; forwarder role fans out).
|
|
963
|
+
registerNotifyHandler(node, reactivityProtocols.notify, notify);
|
|
964
|
+
const offInboundNotify = notify.onNotification((from, n): void => { void forwarderHost.onInbound(from, n); });
|
|
965
|
+
|
|
966
|
+
// 3. Origination emit — install onLocalCommit: a member commit builds a NotificationV1 and ingests it.
|
|
967
|
+
const origination = new ReactivityOriginationManager({
|
|
968
|
+
service: host.service,
|
|
969
|
+
resolveContext: (event) => {
|
|
970
|
+
if (event.tailId === undefined) {
|
|
971
|
+
return undefined; // tail-less (read-driven promotion) never originates (the gate also returns first)
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
// MUST reuse the gate's `reactivityTailBytes` (utf8), NOT db-core's double-hashing
|
|
975
|
+
// blockIdToBytes — else origination derives a different coord than subscribers resolve.
|
|
976
|
+
tailId: reactivityTailBytes(event.tailId),
|
|
977
|
+
deltaMaxBytes: reactivityPolicy.deltaMaxBytes,
|
|
978
|
+
// rotationHint stays undefined on a live node: the successor tail id is not knowable at the
|
|
979
|
+
// filling commit (random block ids; gated on 6.5-block-id-derivation). The authoritative,
|
|
980
|
+
// observable rotation signal is `event.tailId` CHANGING, which the manager observes via the
|
|
981
|
+
// `markRotated` binding below. (The pre-announce remains exercised in the mock-tier harness +
|
|
982
|
+
// the design simulator, both of which can synthesize the successor id.)
|
|
983
|
+
};
|
|
984
|
+
},
|
|
985
|
+
// reactivityNotificationTopicId(n) = reactivityTopicId(b64urlToBytes(n.tailId)); since
|
|
986
|
+
// n.tailId = b64url(reactivityTailBytes(tail)), this is the SAME topicId the gate assembled coord_0
|
|
987
|
+
// around and the subscriber/forwarder verifier derives — closing the encoding loop.
|
|
988
|
+
emit: (n): void => { void forwarderHost.ingest(reactivityNotificationTopicId(n), n); },
|
|
989
|
+
// Observe-rotation: when a collection's tail id changes between commits the OLD tail's reactivity
|
|
990
|
+
// topic has rotated. Start its drain so the recover serve begins redirecting to the new tree (the
|
|
991
|
+
// `reactivity-rotation-recover-redirect-drain` markRotated seam). `oldTopicId` is byte-identical to
|
|
992
|
+
// the topic a subscriber subscribed under (both `reactivityTopicId(reactivityTailBytes(tail))`).
|
|
993
|
+
markRotated: (oldTopicId, redirect, now): void => forwarderHost.markRotated(oldTopicId, redirect, now),
|
|
994
|
+
});
|
|
995
|
+
origination.install();
|
|
996
|
+
|
|
997
|
+
// 4. PushState gossip — periodic intra-cohort convergence so any member (not just the primary) can
|
|
998
|
+
// serve a replay/backfill. Rides the host's cohort gossip transport (no second transport).
|
|
999
|
+
const pushStateGossip = new ReactivityPushStateGossipDriver({
|
|
1000
|
+
gossipTransport: host.gossipTransport,
|
|
1001
|
+
liveCollections: (): ReactivityGossipCollection[] => forwarderHost.livePushStates().map((pushState) => ({
|
|
1002
|
+
pushState,
|
|
1003
|
+
cohortCoord: reactivityAddressing.coord0(b64urlToBytes(pushState.topicId)),
|
|
1004
|
+
})),
|
|
1005
|
+
pushStateForGossip: (g: PushStateGossipV1) => forwarderHost.pushStateFor(b64urlToBytes(g.topicId)),
|
|
1006
|
+
// Authenticity gate: accept gossip only from a member of the cohort around the frame's reactivity
|
|
1007
|
+
// coord (per-frame peer-sig envelope signing is deferred — reactivity-pushstate-gossip's hardening backlog).
|
|
1008
|
+
isCohortMember: (fromPeerId: string, g: PushStateGossipV1): boolean =>
|
|
1009
|
+
fret.assembleCohort(reactivityAddressing.coord0(b64urlToBytes(g.topicId)), cohortWantK).includes(fromPeerId),
|
|
1010
|
+
});
|
|
1011
|
+
registerPushStateGossipHandler(node, reactivityProtocols.pushStateGossip, pushStateGossip);
|
|
1012
|
+
pushStateGossip.start();
|
|
1013
|
+
|
|
1014
|
+
// 5. Recover RPC — the pull companion to notify (docs/reactivity.md §Backfill RPC / §Resume). A
|
|
1015
|
+
// subscriber that detected a gap, or woke from sleep past the live tail, asks a serving cohort member
|
|
1016
|
+
// "what did I miss?" and is brought current over a real request-reply socket. The SERVE side is live
|
|
1017
|
+
// here: this node answers RecoverRequestV1 frames against its live forwarder PushStates. The OUTBOUND
|
|
1018
|
+
// transport + signers are constructed and exposed for the subscribe factory that CONSTRUCTS managers
|
|
1019
|
+
// (the Quereus Database.watch app-bridge — backlog optimystic-network-reactive-watch-integration-test);
|
|
1020
|
+
// no node-internal manager calls them yet, exactly as the notify subscriber side is constructed against
|
|
1021
|
+
// `reactivitySubscribers` rather than from a watch.
|
|
1022
|
+
//
|
|
1023
|
+
// Node-level sticky cohort-hint cache (keyed by collectionId), shared between the outbound transport's
|
|
1024
|
+
// sticky-primary lookup and a future manager's rotation-invalidation so both see ONE cache. It starts
|
|
1025
|
+
// empty ⇒ the transport falls through to the cohort-walk (any member holding the gossiped PushState
|
|
1026
|
+
// answers); populating the sticky primary is a one-RT optimization, not a correctness need.
|
|
1027
|
+
const reactivityCohortHintCache = createStickyCohortHintCache();
|
|
1028
|
+
// topicId → dialable cohort member peer-id strings: the SAME FRET coord_0 assembly the push-state-gossip
|
|
1029
|
+
// authenticity gate uses (`reactivityAddressing.coord0` → `fret.assembleCohort`), so a recover walk
|
|
1030
|
+
// reaches exactly the cohort that holds the topic's gossiped PushState. `assembleCohort` returns peer-id
|
|
1031
|
+
// strings (the recover dialer's `peerIdFromString` space), matching the notify dial-target space.
|
|
1032
|
+
const resolveReactivityCohort = (topicId: Uint8Array): string[] =>
|
|
1033
|
+
fret.assembleCohort(reactivityAddressing.coord0(topicId), cohortWantK);
|
|
1034
|
+
|
|
1035
|
+
// Outbound transport: exposes the db-core BackfillTransport / ResumeTransport seams against this node.
|
|
1036
|
+
// maxBytes is omitted so the dialer + handler default to DEFAULT_STREAM_MAX_BYTES, matching the notify
|
|
1037
|
+
// transport's default (constructed above without an override) — one frame ceiling across the family.
|
|
1038
|
+
const recover = new Libp2pReactivityRecoverTransport({
|
|
1039
|
+
dialer: createLibp2pRecoverDialer(node, reactivityProtocols.recover),
|
|
1040
|
+
selfPeerId,
|
|
1041
|
+
cohortHintCache: reactivityCohortHintCache,
|
|
1042
|
+
resolveCohort: resolveReactivityCohort,
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// Inbound serve handler: decode (bounded) → verify the dialing peer's signature → freshness/replay gate →
|
|
1046
|
+
// resolve the live PushState off the forwarder host → serveBackfill/serveResume → reply (no reply on any
|
|
1047
|
+
// failure; the stream aborts and the subscriber walks/chain-reads). One node-level replay guard is shared
|
|
1048
|
+
// across all recover requests — a plain pruned-on-access map, so no new timer to tear down.
|
|
1049
|
+
registerRecoverHandler(node, reactivityProtocols.recover, {
|
|
1050
|
+
pushStateFor: forwarderHost.pushStateFor.bind(forwarderHost),
|
|
1051
|
+
pushStateForCollection: forwarderHost.pushStateForCollection.bind(forwarderHost),
|
|
1052
|
+
replayGuard: createCorrelationReplayGuard(),
|
|
1053
|
+
rotationFor: (req, now) => {
|
|
1054
|
+
// Drain-window redirect: a recover reaching an OLD (rotated, still-draining) tail is bounced to
|
|
1055
|
+
// the new tree (reactivity-rotation-recover-redirect-drain). A resume carries the stale topic
|
|
1056
|
+
// (topicId = reactivityTopicId(latestKnownTailId)); a backfill carries no topic, so resolve the
|
|
1057
|
+
// collection's current served topic. rotationRedirectFor returns the gate's redirect while
|
|
1058
|
+
// draining and undefined once drained (then evicting the gate + the old tail's served PushState).
|
|
1059
|
+
const oldTopicId = req.topicId ?? resolveCurrentServedTopic(forwarderHost, req.collectionId);
|
|
1060
|
+
return oldTopicId === undefined ? undefined : forwarderHost.rotationRedirectFor(oldTopicId, now);
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
// The subscriber's synchronous request signers over the node's Ed25519 key (resolves the recover wiring's
|
|
1065
|
+
// lone design point — see recover-transport.ts §createRecoverRequestSigners). Fed to a manager by the
|
|
1066
|
+
// subscribe factory alongside recover.backfillTransport(topicId, collectionId) /
|
|
1067
|
+
// recover.resumeTransport(topicId, collectionId).
|
|
1068
|
+
const recoverSigners = createRecoverRequestSigners(nodePrivateKey);
|
|
1069
|
+
|
|
1070
|
+
// Expose the recover seams so the subscribe factory wires backfill/resume RPC + signers + the shared
|
|
1071
|
+
// sticky cache (mirrors `reactivitySubscribers` above).
|
|
1072
|
+
(node as any).reactivityRecover = recover;
|
|
1073
|
+
(node as any).reactivityRecoverSigners = recoverSigners;
|
|
1074
|
+
(node as any).reactivityCohortHintCache = reactivityCohortHintCache;
|
|
1075
|
+
|
|
1076
|
+
// 6. Rotation re-registration scheduler — the host timer that moves a subscriber to the rotated tree
|
|
1077
|
+
// when its manager surfaces a `RotationNotice` (`reactivity-rotation-rereg-scheduler`). Constructed with
|
|
1078
|
+
// the default unref'd `setTimeout` timer so an idle re-registration never pins the process. The
|
|
1079
|
+
// `reRegister(plan)` MOVE belongs to the subscribe factory that CONSTRUCTS managers (the deferred Quereus
|
|
1080
|
+
// `Database.watch` bridge — backlog optimystic-network-reactive-watch-integration-test): on fire it builds
|
|
1081
|
+
// a fresh `ReactivitySubscriptionManager` under `plan.newTopicId` carrying `plan.lastRevision`, registers
|
|
1082
|
+
// it, and swaps the `ReactivitySubscriberRegistry` entry — registering the NEW-topic handler BEFORE
|
|
1083
|
+
// unregistering the old, so a notification mid-swap is never dropped. Until that factory lands no
|
|
1084
|
+
// node-internal manager drives `schedule()`, so this seam is a logged no-op — exactly as 12.33 exposed
|
|
1085
|
+
// `reactivitySubscribers` / `reactivityRecover` without a live manager constructor.
|
|
1086
|
+
const reactivityRotation = new RotationReRegistrationScheduler({
|
|
1087
|
+
reRegister: (plan): Promise<void> => {
|
|
1088
|
+
reactivityWiringLog("reactivity rotation re-registration fired for successor topic=%s (lastRevision=%d) but no subscribe factory is wired yet — deferred to optimystic-network-reactive-watch-integration-test", bytesToB64url(plan.newTopicId), plan.lastRevision);
|
|
1089
|
+
return Promise.resolve();
|
|
1090
|
+
},
|
|
1091
|
+
});
|
|
1092
|
+
(node as any).reactivityRotation = reactivityRotation;
|
|
1093
|
+
|
|
1094
|
+
// --- Matchmaking QueryV1 RPC — cohort serve side (docs/matchmaking.md §Seeker query) ---
|
|
1095
|
+
// The server half of the seeker query transport: a remote seeker dials `/optimystic/matchmaking/1.0.0/query`
|
|
1096
|
+
// and this node answers with its cohort's locally-held provider/seeker registrations, signed by the node
|
|
1097
|
+
// peer key. Matchmaking is layered ABOVE the cohort-topic substrate, so it owns its own protocol family
|
|
1098
|
+
// and is wired here (the composition root) over the host's PUBLIC surface only — mirroring the reactivity
|
|
1099
|
+
// registration above; nothing reaches into host.ts internals. The OUTBOUND seeker walk client is the
|
|
1100
|
+
// prereq follow-on `matchmaking-query-rpc-seeker-walk`; only the serve side is live here.
|
|
1101
|
+
const matchmakingProtocols = DEFAULT_MATCHMAKING_PROTOCOLS;
|
|
1102
|
+
registerMatchmakingQueryHandler(node, matchmakingProtocols.query, {
|
|
1103
|
+
registry: host.registry,
|
|
1104
|
+
// Reuse the reactivity addressing: createTierAddressing(createRingHash()) is byte-identical to the
|
|
1105
|
+
// host's internal addressing for the tier-0 coord (peer- and fanout-independent), and the handler
|
|
1106
|
+
// only ever derives coord_0(topicId).
|
|
1107
|
+
addressing: reactivityAddressing,
|
|
1108
|
+
// Single-member reply signature over the node peer key (same pattern reactivity uses for its signers).
|
|
1109
|
+
sign: async (payload: Uint8Array): Promise<string> => bytesToB64url(await signPeer(nodePrivateKey, payload)),
|
|
1110
|
+
// Anti-DoS rate-limit seam (backlog matchmaking-query-rate-limit) intentionally left unwired here:
|
|
1111
|
+
// default-allow. When that ticket lands it passes a `gate: (from, topicId) => boolean` that limits on
|
|
1112
|
+
// the connection's verified `from` peer (NOT the self-asserted query.requesterId).
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
// Teardown: release reactivity timers + protocol handlers BEFORE host.stop() (which clears the cohort
|
|
1116
|
+
// gossip timer + unhandles the cohort-topic protocols) BEFORE the node's transports close (previousStop).
|
|
1117
|
+
// Composes with the existing arachnode + clusterMember stop wrappers (each calls its captured previousStop last).
|
|
1118
|
+
const previousStop = node.stop.bind(node);
|
|
1119
|
+
node.stop = async (): Promise<void> => {
|
|
1120
|
+
try {
|
|
1121
|
+
reactivityRotation.stop();
|
|
1122
|
+
pushStateGossip.stop();
|
|
1123
|
+
offInboundNotify();
|
|
1124
|
+
await node.unhandle(reactivityProtocolList(reactivityProtocols));
|
|
1125
|
+
await node.unhandle(matchmakingProtocolList(matchmakingProtocols));
|
|
1126
|
+
unsubscribe();
|
|
1127
|
+
await host.stop();
|
|
1128
|
+
} finally {
|
|
1129
|
+
await previousStop();
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
|
|
526
1134
|
return node;
|
|
527
1135
|
}
|