@optimystic/db-p2p 0.24.2 → 0.25.0
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 → README.md} +688 -668
- package/dist/src/cluster/block-transfer-service.d.ts +133 -10
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +155 -9
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +67 -7
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +127 -24
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/certified-claims.d.ts +170 -0
- package/dist/src/cluster/certified-claims.d.ts.map +1 -0
- package/dist/src/cluster/certified-claims.js +200 -0
- package/dist/src/cluster/certified-claims.js.map +1 -0
- package/dist/src/cluster/client-signature-verifier.d.ts +23 -0
- package/dist/src/cluster/client-signature-verifier.d.ts.map +1 -0
- package/dist/src/cluster/client-signature-verifier.js +33 -0
- package/dist/src/cluster/client-signature-verifier.js.map +1 -0
- package/dist/src/cluster/cluster-policy.d.ts +9 -3
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js +111 -27
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +140 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +485 -29
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.d.ts +134 -0
- package/dist/src/cluster/commit-proof.d.ts.map +1 -0
- package/dist/src/cluster/commit-proof.js +263 -0
- package/dist/src/cluster/commit-proof.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +95 -8
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +113 -14
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +84 -0
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +207 -5
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts +27 -7
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +179 -38
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/service.d.ts +13 -4
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +17 -18
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +8 -2
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +7 -7
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -7
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +7 -6
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +14 -41
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/cascade.d.ts +7 -8
- package/dist/src/dispute/cascade.d.ts.map +1 -1
- package/dist/src/dispute/cascade.js +24 -7
- package/dist/src/dispute/cascade.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +5 -0
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +13 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +33 -15
- package/dist/src/dispute/invalidation.d.ts.map +1 -1
- package/dist/src/dispute/invalidation.js +127 -34
- package/dist/src/dispute/invalidation.js.map +1 -1
- package/dist/src/dispute/service.d.ts.map +1 -1
- package/dist/src/dispute/service.js +2 -1
- package/dist/src/dispute/service.js.map +1 -1
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +14 -10
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +28 -58
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +38 -7
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +148 -63
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/network/open-protocol-stream.d.ts +56 -0
- package/dist/src/network/open-protocol-stream.d.ts.map +1 -0
- package/dist/src/network/open-protocol-stream.js +60 -0
- package/dist/src/network/open-protocol-stream.js.map +1 -0
- package/dist/src/network/register-protocol-handler.d.ts +66 -0
- package/dist/src/network/register-protocol-handler.d.ts.map +1 -0
- package/dist/src/network/register-protocol-handler.js +51 -0
- package/dist/src/network/register-protocol-handler.js.map +1 -0
- package/dist/src/peer-address-book.d.ts +60 -2
- package/dist/src/peer-address-book.d.ts.map +1 -1
- package/dist/src/peer-address-book.js +89 -1
- package/dist/src/peer-address-book.js.map +1 -1
- package/dist/src/pend-validation.d.ts +65 -0
- package/dist/src/pend-validation.d.ts.map +1 -0
- package/dist/src/pend-validation.js +69 -0
- package/dist/src/pend-validation.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts.map +1 -1
- package/dist/src/reactivity/notify-transport.js +2 -1
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -1
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +32 -6
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +44 -10
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +181 -25
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +650 -62
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/served-repo-proxy.d.ts +30 -0
- package/dist/src/repo/served-repo-proxy.d.ts.map +1 -0
- package/dist/src/repo/served-repo-proxy.js +50 -0
- package/dist/src/repo/served-repo-proxy.js.map +1 -0
- package/dist/src/repo/service.d.ts +16 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +18 -16
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/reputation/types.d.ts +3 -1
- package/dist/src/reputation/types.d.ts.map +1 -1
- package/dist/src/reputation/types.js +3 -1
- package/dist/src/reputation/types.js.map +1 -1
- package/dist/src/rn.d.ts +24 -2
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +33 -4
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-archive.d.ts +153 -0
- package/dist/src/storage/block-archive.d.ts.map +1 -0
- package/dist/src/storage/block-archive.js +194 -0
- package/dist/src/storage/block-archive.js.map +1 -0
- package/dist/src/storage/block-latch.d.ts +92 -0
- package/dist/src/storage/block-latch.d.ts.map +1 -0
- package/dist/src/storage/block-latch.js +132 -0
- package/dist/src/storage/block-latch.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +169 -12
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +415 -152
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +5 -1
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/cached-raw-storage.js +16 -1
- package/dist/src/storage/cached-raw-storage.js.map +1 -1
- package/dist/src/storage/cached-store-driver.d.ts +14 -4
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
- package/dist/src/storage/cached-store-driver.js +39 -5
- package/dist/src/storage/cached-store-driver.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +70 -19
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js +21 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +38 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.d.ts +14 -8
- package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.js +29 -9
- package/dist/src/storage/kv-raw-storage.js.map +1 -1
- package/dist/src/storage/memory-store-driver.d.ts +4 -1
- package/dist/src/storage/memory-store-driver.d.ts.map +1 -1
- package/dist/src/storage/memory-store-driver.js +22 -2
- package/dist/src/storage/memory-store-driver.js.map +1 -1
- package/dist/src/storage/raw-store-codec.js +2 -2
- package/dist/src/storage/raw-store-driver.d.ts +44 -3
- package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +60 -3
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.js +68 -4
- package/dist/src/storage/shared-cache-pool.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +204 -26
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +422 -119
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/store-identity.d.ts +21 -0
- package/dist/src/storage/store-identity.d.ts.map +1 -0
- package/dist/src/storage/store-identity.js +33 -0
- package/dist/src/storage/store-identity.js.map +1 -0
- package/dist/src/storage/struct.d.ts +15 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/storage/with-read-cache.d.ts +92 -0
- package/dist/src/storage/with-read-cache.d.ts.map +1 -0
- package/dist/src/storage/with-read-cache.js +132 -0
- package/dist/src/storage/with-read-cache.js.map +1 -0
- package/dist/src/sync/service.d.ts +13 -3
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +7 -30
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +75 -2
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +193 -81
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +8 -0
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +97 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +5 -6
- package/src/cluster/block-transfer-service.ts +528 -312
- package/src/cluster/block-transfer.ts +547 -424
- package/src/cluster/certified-claims.ts +319 -0
- package/src/cluster/client-signature-verifier.ts +34 -0
- package/src/cluster/cluster-policy.ts +294 -203
- package/src/cluster/cluster-repo.ts +2501 -2034
- package/src/cluster/commit-proof.ts +344 -0
- package/src/cluster/quorum-restore.ts +348 -223
- package/src/cluster/rebalance-monitor.ts +539 -254
- package/src/cluster/reconcile-block.ts +404 -203
- package/src/cluster/service.ts +23 -20
- package/src/cluster/spread-on-churn.ts +352 -346
- package/src/cohort-topic/host.ts +8 -7
- package/src/cohort-topic/stream-util.ts +14 -43
- package/src/dispute/cascade.ts +31 -15
- package/src/dispute/dispute-service.ts +706 -696
- package/src/dispute/invalidation.ts +162 -52
- package/src/dispute/service.ts +126 -125
- package/src/index.ts +7 -0
- package/src/libp2p-key-network.ts +1204 -1235
- package/src/libp2p-node-base.ts +185 -70
- package/src/network/open-protocol-stream.ts +87 -0
- package/src/network/register-protocol-handler.ts +85 -0
- package/src/peer-address-book.ts +96 -2
- package/src/pend-validation.ts +89 -0
- package/src/reactivity/notify-transport.ts +2 -1
- package/src/reactivity/push-state-gossip.ts +2 -1
- package/src/repo/cluster-coordinator.ts +65 -11
- package/src/repo/coordinator-repo.ts +729 -71
- package/src/repo/served-repo-proxy.ts +55 -0
- package/src/repo/service.ts +25 -16
- package/src/reputation/types.ts +3 -1
- package/src/rn.ts +34 -4
- package/src/storage/block-archive.ts +242 -0
- package/src/storage/block-latch.ts +144 -0
- package/src/storage/block-storage.ts +787 -498
- package/src/storage/cached-raw-storage.ts +21 -1
- package/src/storage/cached-store-driver.ts +898 -859
- package/src/storage/i-block-storage.ts +172 -116
- package/src/storage/i-raw-storage.ts +40 -0
- package/src/storage/kv-raw-storage.ts +34 -9
- package/src/storage/memory-store-driver.ts +26 -2
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +45 -3
- package/src/storage/shared-cache-pool.ts +516 -452
- package/src/storage/storage-repo.ts +1421 -1014
- package/src/storage/store-identity.ts +41 -0
- package/src/storage/struct.ts +52 -34
- package/src/storage/with-read-cache.ts +182 -0
- package/src/sync/service.ts +178 -198
- package/src/testing/mesh-harness.ts +524 -344
- package/src/testing/raw-storage-conformance.ts +114 -2
package/src/libp2p-node-base.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { identify, identifyPush } from '@libp2p/identify';
|
|
|
5
5
|
import { ping } from '@libp2p/ping';
|
|
6
6
|
import { dcutr } from '@libp2p/dcutr';
|
|
7
7
|
import { autoNAT } from '@libp2p/autonat';
|
|
8
|
-
import { gossipsub } from '@chainsafe/libp2p-gossipsub';
|
|
9
8
|
import { bootstrap } from '@libp2p/bootstrap';
|
|
10
9
|
import { circuitRelayServer, type CircuitRelayServerInit } from '@libp2p/circuit-relay-v2';
|
|
11
10
|
import { peerIdFromString } from '@libp2p/peer-id';
|
|
@@ -14,10 +13,13 @@ import type { ConnectionGater, PrivateKey } from '@libp2p/interface';
|
|
|
14
13
|
import { clusterService } from './cluster/service.js';
|
|
15
14
|
import { blockTransferService } from './cluster/block-transfer-service.js';
|
|
16
15
|
import { repoService } from './repo/service.js';
|
|
17
|
-
import { StorageRepo
|
|
16
|
+
import { StorageRepo } from './storage/storage-repo.js';
|
|
18
17
|
import { BlockStorage } from './storage/block-storage.js';
|
|
19
18
|
import { MemoryRawStorage } from './storage/memory-storage.js';
|
|
19
|
+
import { withReadCache, type ResolvedReadCache } from './storage/with-read-cache.js';
|
|
20
20
|
import type { IRawStorage } from './storage/i-raw-storage.js';
|
|
21
|
+
import { latestClaimFromArchive, servableProof, type ArchiveServingRepo } from './storage/block-archive.js';
|
|
22
|
+
import { createServedRepoProxy } from './repo/served-repo-proxy.js';
|
|
21
23
|
import { seedOwnedBlocksFromStorage } from './owned-block-seed.js';
|
|
22
24
|
import { clusterMember, type ReconcileBlockCallback, type CommitCertificateSink, type DeriveExpectedClusterCallback } from './cluster/cluster-repo.js';
|
|
23
25
|
import { createReconcileBlock } from './cluster/reconcile-block.js';
|
|
@@ -26,7 +28,7 @@ import { assertClusterSizeCoupling } from './cluster/cluster-size-coupling.js';
|
|
|
26
28
|
import { createCommitCertStore, makeClusterCommitCertExtractor, type CommitCertStore } from './cluster/commit-cert.js';
|
|
27
29
|
import { coordinatorRepo } from './repo/coordinator-repo.js';
|
|
28
30
|
import { Libp2pKeyPeerNetwork, type NetworkMode, type NetworkStatePersistence } from './libp2p-key-network.js';
|
|
29
|
-
import { mergePeerAddresses,
|
|
31
|
+
import { mergePeerAddresses, publishableAddrsForPeer, type AddressLog } from './peer-address-book.js';
|
|
30
32
|
import type { OptimysticNode, OptimysticNodeAttachments } from './optimystic-node.js';
|
|
31
33
|
import { ClusterClient } from './cluster/client.js';
|
|
32
34
|
import type { IRepo, ICluster, ITransactionValidator, BlockId, IBlockChangeNotifier } from '@optimystic/db-core';
|
|
@@ -135,7 +137,18 @@ const reactivityWiringLog = createLogger('reactivity-node-wiring');
|
|
|
135
137
|
*/
|
|
136
138
|
const wiringLog = createLogger('node-wiring');
|
|
137
139
|
|
|
138
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* Factory function or instance for creating raw storage. The node puts the write-through read
|
|
142
|
+
* cache in front of the resolved instance (`withReadCache`, unless it is a `MemoryRawStorage` or
|
|
143
|
+
* already cached) under a lease, and releases THAT LEASE when it stops; the instance you supplied
|
|
144
|
+
* is never disposed. The cache is shared per backing store: two concurrently running nodes handed
|
|
145
|
+
* one uncached instance — or two instances that report the same `getStoreIdentity()`, such as two
|
|
146
|
+
* `FileRawStorage` over one directory — read and write through ONE cache, which is cleared and
|
|
147
|
+
* unregistered only when the last of them stops. Sequential reuse (a restart over the same
|
|
148
|
+
* instance) starts cold once the previous node's lease has released. What remains unguarded is
|
|
149
|
+
* the cross-process case (Invariant 5 in `packages/db-p2p/docs/storage.md`). A host that builds
|
|
150
|
+
* its own `CachedRawStorage` and hands it in keeps owning it; the node never releases it.
|
|
151
|
+
*/
|
|
139
152
|
export type RawStorageProvider = IRawStorage | (() => IRawStorage);
|
|
140
153
|
|
|
141
154
|
/**
|
|
@@ -180,7 +193,7 @@ export type NodeOptions = ClusterPolicyOptions & {
|
|
|
180
193
|
* `{ reservations: { applyDefaultLimit: false } }` to lift the cap.
|
|
181
194
|
*/
|
|
182
195
|
relayServerInit?: CircuitRelayServerInit;
|
|
183
|
-
/** Storage provider - either an IRawStorage instance or a factory function. Defaults to MemoryRawStorage if not provided. */
|
|
196
|
+
/** Storage provider - either an IRawStorage instance or a factory function. Defaults to MemoryRawStorage if not provided. See {@link RawStorageProvider} for the ownership rule. */
|
|
184
197
|
storage?: RawStorageProvider;
|
|
185
198
|
/** Override libp2p listen multiaddrs. */
|
|
186
199
|
listenAddrs?: string[];
|
|
@@ -225,11 +238,15 @@ export type NodeOptions = ClusterPolicyOptions & {
|
|
|
225
238
|
|
|
226
239
|
/**
|
|
227
240
|
* Rebalance reaction tuning. Drives the RebalanceMonitor + BlockTransferCoordinator pull-gained/
|
|
228
|
-
* push-lost path when arachnode/FRET are available (the only place fretAdapter +
|
|
229
|
-
* coordinator exist).
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
241
|
+
* push-lost/replicate-grown path when arachnode/FRET are available (the only place fretAdapter +
|
|
242
|
+
* restoration coordinator exist). The grown arm is what pushes a block this node keeps to peers
|
|
243
|
+
* that newly became co-responsible for it — the founder case: anything committed while the
|
|
244
|
+
* deployment was one node gets its second copy only through this path (bounded per pass by
|
|
245
|
+
* `growthBlockBudget`). Absent -> enabled with defaults (see RebalanceMonitorConfig). Set
|
|
246
|
+
* { enabled: false } to disable the rebalance reaction (including the grown arm) on this node.
|
|
247
|
+
* When arachnode is disabled or FRET is absent the rebalance path stays inert regardless of this
|
|
248
|
+
* flag (rebalance is a resilience optimization — except for singly-held blocks, where the grown
|
|
249
|
+
* arm is currently the only mechanism that ever creates a second copy).
|
|
233
250
|
*/
|
|
234
251
|
rebalance?: Partial<RebalanceMonitorConfig> & { enabled?: boolean };
|
|
235
252
|
|
|
@@ -242,6 +259,21 @@ export type NodeOptions = ClusterPolicyOptions & {
|
|
|
242
259
|
/** Dispute protocol configuration */
|
|
243
260
|
dispute?: Partial<DisputeConfig>;
|
|
244
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Block-transfer (churn re-replication) receiver tuning.
|
|
264
|
+
*
|
|
265
|
+
* `requirePushCertificate` (default `true`) refuses a pushed block that carries no verifying
|
|
266
|
+
* cohort commit proof — see `BlockTransferServiceInit.requirePushCertificate` for why that is
|
|
267
|
+
* the default. It is exposed here because the migration it exists for is a DEPLOYMENT decision
|
|
268
|
+
* and the escape hatch lives on the RECEIVER: a node still holding pre-proof blocks cannot
|
|
269
|
+
* certify them (the signatures no longer exist), so under the strict default those blocks never
|
|
270
|
+
* gain a new holder — which also means `BlockTransferCoordinator.confirmReplicated` never
|
|
271
|
+
* confirms them, so a rebalance never releases them and a ring shift's confirm phase aborts on
|
|
272
|
+
* them (`docs/arachnode-ring-handoff.md` § Phase B). Clearing the backlog means running the
|
|
273
|
+
* receivers with this `false` until every such block has been rewritten under current code.
|
|
274
|
+
*/
|
|
275
|
+
blockTransfer?: { requirePushCertificate?: boolean };
|
|
276
|
+
|
|
245
277
|
/** Optional persistent store for 2PC transaction state (enables crash recovery) */
|
|
246
278
|
transactionStateStore?: ITransactionStateStore;
|
|
247
279
|
|
|
@@ -327,11 +359,30 @@ export type NodeOptions = ClusterPolicyOptions & {
|
|
|
327
359
|
connectionGater?: ConnectionGater;
|
|
328
360
|
};
|
|
329
361
|
|
|
330
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Resolve the node's raw storage and put the write-through read cache in front of it. This is
|
|
364
|
+
* the single place the network node resolves its `IRawStorage`, so it is the single place the
|
|
365
|
+
* cache is wired (`withReadCache` states the exclusions: memory storage and already-cached
|
|
366
|
+
* storage pass through unchanged). The default is a bare `MemoryRawStorage`, deliberately not
|
|
367
|
+
* routed through the helper — nothing to cache.
|
|
368
|
+
*
|
|
369
|
+
* `lease` is this node's claim on the (possibly shared) cache, and the ONLY thing its stop path
|
|
370
|
+
* may release — a host that supplied its own `CachedRawStorage` keeps owning it (see
|
|
371
|
+
* {@link ResolvedReadCache}).
|
|
372
|
+
*/
|
|
373
|
+
function resolveStorage(provider: RawStorageProvider | undefined, networkName: string): ResolvedReadCache {
|
|
331
374
|
if (!provider) {
|
|
332
|
-
return new MemoryRawStorage();
|
|
375
|
+
return { storage: new MemoryRawStorage(), lease: undefined };
|
|
333
376
|
}
|
|
334
|
-
|
|
377
|
+
const storage = typeof provider === 'function' ? provider() : provider;
|
|
378
|
+
// NOTE: the label is the network name, so N nodes on one network in one process over N DISTINCT
|
|
379
|
+
// stores produce N identically-labelled rows in `SharedCachePool.stats()` (the pool keys on a
|
|
380
|
+
// monotonic store id, not the label); N nodes over ONE store share a single row, labelled by
|
|
381
|
+
// whichever node wrapped first. Harmless while the label is only read by a human eyeballing
|
|
382
|
+
// occupancy; if pool stats ever need to attribute bytes to a SPECIFIC node, fold the peer id
|
|
383
|
+
// in — it is not known here, so that would mean labelling after node construction rather than
|
|
384
|
+
// at resolve time.
|
|
385
|
+
return withReadCache(storage, `node:${networkName}`);
|
|
335
386
|
}
|
|
336
387
|
|
|
337
388
|
/**
|
|
@@ -374,7 +425,7 @@ export async function createLibp2pNodeBase(
|
|
|
374
425
|
transports: Libp2pTransports;
|
|
375
426
|
}
|
|
376
427
|
): Promise<OptimysticNode> {
|
|
377
|
-
const rawStorage = resolveStorage(options.storage);
|
|
428
|
+
const { storage: rawStorage, lease } = resolveStorage(options.storage, options.networkName);
|
|
378
429
|
|
|
379
430
|
// Create placeholder restore callback (will be replaced after node starts)
|
|
380
431
|
let restoreCallback: RestoreCallback = async (_blockId, _rev?) => {
|
|
@@ -386,15 +437,6 @@ export async function createLibp2pNodeBase(
|
|
|
386
437
|
new BlockStorage(blockId, rawStorage, restoreCallback)
|
|
387
438
|
);
|
|
388
439
|
|
|
389
|
-
// Per-block commit-latch runner, ready to thread into the invalidation-apply sink (`onInvalidate`)
|
|
390
|
-
// passed to `clusterMember(...)` and into each cascade `CollectionEnv`, the instant either is wired
|
|
391
|
-
// here. Sharing the `StorageRepo.commit:<blockId>` latch makes a compensating saveReplica/saveDeletion
|
|
392
|
-
// RMW of `meta.latest` mutually exclusive with a concurrent commit on the same block. It is unused
|
|
393
|
-
// today only because no `onInvalidate`/cascade driver is wired in the live node (see review handoff);
|
|
394
|
-
// it is bound here so that wiring is a one-liner and cannot reach for a divergent latch key.
|
|
395
|
-
const blockCommitLatch = withBlockCommitLatch;
|
|
396
|
-
void blockCommitLatch;
|
|
397
|
-
|
|
398
440
|
let clusterImpl: ICluster | undefined;
|
|
399
441
|
let coordinatedRepo: IRepo | undefined;
|
|
400
442
|
// The running node, bound immediately after `createLibp2p` below. Service factories that need
|
|
@@ -415,24 +457,11 @@ export async function createLibp2pNodeBase(
|
|
|
415
457
|
}
|
|
416
458
|
};
|
|
417
459
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
async pend(request, options) {
|
|
424
|
-
const target = coordinatedRepo ?? storageRepo;
|
|
425
|
-
return await target.pend(request, options);
|
|
426
|
-
},
|
|
427
|
-
async cancel(trxRef, options) {
|
|
428
|
-
const target = coordinatedRepo ?? storageRepo;
|
|
429
|
-
return await target.cancel(trxRef, options);
|
|
430
|
-
},
|
|
431
|
-
async commit(request, options) {
|
|
432
|
-
const target = coordinatedRepo ?? storageRepo;
|
|
433
|
-
return await target.commit(request, options);
|
|
434
|
-
}
|
|
435
|
-
};
|
|
460
|
+
// Built by a named factory, not an inline literal: see `createServedRepoProxy` for why (an
|
|
461
|
+
// inline object here is unreachable from every test that does not boot a libp2p node, which is
|
|
462
|
+
// how it served every repair archive without its commit proof). `coordinatedRepo` is read per
|
|
463
|
+
// call because it is assigned further down, after the cluster is assembled.
|
|
464
|
+
const repoProxy: ArchiveServingRepo = createServedRepoProxy(storageRepo, () => coordinatedRepo);
|
|
436
465
|
|
|
437
466
|
// The ONE authorization slice, spread verbatim into all four database-protocol service inits
|
|
438
467
|
// below. Building it once (rather than repeating two option reads per service) is what makes
|
|
@@ -547,10 +576,17 @@ export async function createLibp2pNodeBase(
|
|
|
547
576
|
// is acceptable — they neither throw nor break the build in that case.
|
|
548
577
|
dcutr: dcutr(),
|
|
549
578
|
autoNAT: autoNAT(),
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
579
|
+
// NOTE: no `pubsub` service. `@chainsafe/libp2p-gossipsub` (currently at 14.1.2, the newest
|
|
580
|
+
// published version) is built against `@libp2p/interface@^2` — its `Stream` still expects
|
|
581
|
+
// `.sink`/`.source`. Under libp2p 3 `Stream extends MessageStream` (event target +
|
|
582
|
+
// AsyncIterable), which has neither, so `it-pipe`'s `isDuplex` check on the outbound stream
|
|
583
|
+
// fails and `OutboundStream`'s constructor throws `fns.shift(...) is not a function`
|
|
584
|
+
// synchronously — before gossipsub's own `.catch` can attach. Gossipsub logs and swallows
|
|
585
|
+
// it, so the service reports as running while never sending a subscription to any peer:
|
|
586
|
+
// `publish` always resolves with zero recipients. Verified end-to-end against this repo's
|
|
587
|
+
// installed dependency tree (gotchoices/Optimystic#9). Nothing in this repo uses `pubsub`
|
|
588
|
+
// today — re-add only against a gossipsub release built for libp2p 3.
|
|
589
|
+
|
|
554
590
|
// Circuit relay server - enables this node to relay connections for other peers
|
|
555
591
|
...(options.relay ? { relay: circuitRelayServer(options.relayServerInit) } : {}),
|
|
556
592
|
|
|
@@ -571,17 +607,13 @@ export async function createLibp2pNodeBase(
|
|
|
571
607
|
peerId: components.peerId,
|
|
572
608
|
// Fallback addr resolver for redirect targets whose multiaddrs are not
|
|
573
609
|
// already embedded in record.peers. A redirect payload is handed to a THIRD
|
|
574
|
-
// party, so it
|
|
575
|
-
// `
|
|
576
|
-
//
|
|
577
|
-
getConnectionAddrs: (peerId: any) => {
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
const addr = publishableConnectionAddr(c, addressLog);
|
|
582
|
-
if (addr !== undefined) addrs.push(addr);
|
|
583
|
-
}
|
|
584
|
-
return addrs;
|
|
610
|
+
// party, so it answers with `publishableAddrsForPeer` — the same rule, and the
|
|
611
|
+
// same function, `findCluster` uses to fill a cluster record: the publishable
|
|
612
|
+
// half of our live connections plus the peer's own advertised addresses.
|
|
613
|
+
getConnectionAddrs: async (peerId: any) => {
|
|
614
|
+
if (!liveNode) return [];
|
|
615
|
+
const conns = liveNode.getConnections?.(peerId) ?? [];
|
|
616
|
+
return await publishableAddrsForPeer(liveNode, conns, peerId, addressLog);
|
|
585
617
|
},
|
|
586
618
|
// Inbound cluster records carry each cohort member's multiaddrs. libp2p only
|
|
587
619
|
// propagates addresses between directly-connected peers, so for a cohort chosen
|
|
@@ -635,11 +667,20 @@ export async function createLibp2pNodeBase(
|
|
|
635
667
|
blockTransfer: (components: any) => {
|
|
636
668
|
const serviceFactory = blockTransferService({
|
|
637
669
|
protocolPrefix: `/optimystic/${options.networkName}`,
|
|
670
|
+
// Absent → the strict default inside the service; see `blockTransfer` on the options.
|
|
671
|
+
...(options.blockTransfer?.requirePushCertificate !== undefined
|
|
672
|
+
? { requirePushCertificate: options.blockTransfer.requirePushCertificate }
|
|
673
|
+
: {}),
|
|
638
674
|
...inboundAuthorization
|
|
639
675
|
});
|
|
640
676
|
return serviceFactory({
|
|
641
677
|
registrar: components.registrar,
|
|
642
678
|
repo: storageRepo,
|
|
679
|
+
// Verifying a pushed block's commit proof needs the cohort's super-majority fraction.
|
|
680
|
+
// Read from the SAME resolved `consensusConfig` the member and coordinator read (whose
|
|
681
|
+
// coupling `assertSuperMajorityCoupling` below already asserts) — a third copy resolving
|
|
682
|
+
// its own default would defeat that.
|
|
683
|
+
superMajorityThreshold: consensusConfig.superMajorityThreshold,
|
|
643
684
|
// So this service's authorization denials reach the same error sink as the other three.
|
|
644
685
|
logger: components.logger
|
|
645
686
|
});
|
|
@@ -694,6 +735,26 @@ export async function createLibp2pNodeBase(
|
|
|
694
735
|
// soon as the protocol handler goes live in start().
|
|
695
736
|
liveNode = node;
|
|
696
737
|
|
|
738
|
+
// Release this node's lease on the raw-storage read cache when the node stops. Installed FIRST
|
|
739
|
+
// — before start() and before every other stop wrapper — so it runs LAST in the wrapper chain,
|
|
740
|
+
// after every monitor and service that may still read storage during its own stop. Only wired
|
|
741
|
+
// when the seam handed out a lease: a MemoryRawStorage passes through unwrapped, and a
|
|
742
|
+
// host-supplied `CachedRawStorage` stays the host's to dispose. Releasing a lease is safe even
|
|
743
|
+
// when other nodes share the cache — it is cleared and unregistered only when the last lease
|
|
744
|
+
// goes, so stopping one node never blinds the others. A skipped release leaks only cold
|
|
745
|
+
// entries the pool evicts under pressure; the point of the polite release is honest pool
|
|
746
|
+
// occupancy on a long-lived provider node.
|
|
747
|
+
if (lease) {
|
|
748
|
+
const previousStop = node.stop.bind(node);
|
|
749
|
+
node.stop = async () => {
|
|
750
|
+
try {
|
|
751
|
+
await previousStop();
|
|
752
|
+
} finally {
|
|
753
|
+
await lease.release();
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
697
758
|
// Inject the REAL libp2p node into the services that need it, before start(). These are
|
|
698
759
|
// load-bearing and the node has NOT started yet, so any throw fails fast and rejects node
|
|
699
760
|
// creation (nothing started leaks) — far better than the service silently falling back to the
|
|
@@ -777,10 +838,21 @@ export async function createLibp2pNodeBase(
|
|
|
777
838
|
const reconcileBlock: ReconcileBlockCallback = createReconcileBlock({
|
|
778
839
|
selfPeerId: node.peerId.toString(),
|
|
779
840
|
fetchArchive: fetchArchiveFromPeer,
|
|
780
|
-
|
|
841
|
+
// The 4th parameter is a proof reconcile verified against these exact bytes; it is
|
|
842
|
+
// persisted so the repaired replica serves it onward (see StorageRepo.saveReplicatedBlock).
|
|
843
|
+
saveReplicatedBlock: (blockId, block, source, verifiedProof) =>
|
|
844
|
+
storageRepo.saveReplicatedBlock(blockId, block, source, verifiedProof),
|
|
781
845
|
simpleMajorityThreshold: consensusConfig.simpleMajorityThreshold,
|
|
846
|
+
superMajorityThreshold: consensusConfig.superMajorityThreshold,
|
|
782
847
|
repairCorroborationClusterSize: consensusConfig.repairCorroborationClusterSize,
|
|
783
848
|
reputation
|
|
849
|
+
// `anchoring` (proof layer-2, `ProofAnchoring`) is intentionally NOT wired here yet — nor
|
|
850
|
+
// is the coordinator's `proofAnchoring` below: a real implementation re-derives the
|
|
851
|
+
// block's cohort from `keyNetwork.findCluster` (the same source `deriveExpectedCluster`
|
|
852
|
+
// uses) and needs a churn-tolerance window so historic cohort rotation does not read as an
|
|
853
|
+
// anomaly flood. Until then certification runs on layer-1 cryptography and LOGS the
|
|
854
|
+
// unanchored residual. See `feat-cluster-membership-threshold-cert-anchoring`, and the
|
|
855
|
+
// matching `recomputeArbitratorSet` note at the clusterMember construction below.
|
|
784
856
|
});
|
|
785
857
|
|
|
786
858
|
// Member-side membership derivation for the admission gate: independently re-derive this block's
|
|
@@ -855,12 +927,18 @@ export async function createLibp2pNodeBase(
|
|
|
855
927
|
);
|
|
856
928
|
|
|
857
929
|
// Create callback for querying cluster peers for their latest block revision. Three-way
|
|
858
|
-
// contract (see ClusterLatestCallback):
|
|
930
|
+
// contract (see ClusterLatestCallback): a CertifiedActionRev is the peer's claim, a resolved
|
|
859
931
|
// `undefined` is the peer answering "I hold nothing", and a REJECTION is silence — the
|
|
860
932
|
// coordinator counts it as "did not answer" and refuses to report an authoritative absent
|
|
861
933
|
// over it. Transport errors must therefore propagate, not collapse into `undefined` (that
|
|
862
934
|
// collapse let a slow two-node cohort report a missing block as authoritatively absent —
|
|
863
935
|
// ticket cluster-read-consult-cannot-report-unreachable).
|
|
936
|
+
//
|
|
937
|
+
// The claim carries the cohort's commit proof for the claimed revision when the answering
|
|
938
|
+
// peer retained one. It is attached UNVERIFIED — a peer chooses what to send. Both repair
|
|
939
|
+
// paths verify it before weighing it (the shared layer in `cluster/certified-claims.ts`):
|
|
940
|
+
// the coordinator's certifyClaim pass in `queryClusterForLatest`, and reconcile's
|
|
941
|
+
// certification pass in `cluster/reconcile-block.ts`.
|
|
864
942
|
const clusterLatestCallback: ClusterLatestCallback = async (peerId, blockId, context?) => {
|
|
865
943
|
// Self-read short-circuit: dialling self via SyncClient is a round trip
|
|
866
944
|
// with no remote on the other end, and on nodes without listen addresses
|
|
@@ -871,7 +949,13 @@ export async function createLibp2pNodeBase(
|
|
|
871
949
|
if (peerId.equals(node.peerId)) {
|
|
872
950
|
try {
|
|
873
951
|
const result = await storageRepo.get({ blockIds: [blockId], context });
|
|
874
|
-
|
|
952
|
+
const latest = result[blockId]?.state?.latest;
|
|
953
|
+
if (!latest) return undefined;
|
|
954
|
+
// The SAME lookup a peer serving an archive uses, so a self answer and a remote
|
|
955
|
+
// answer attach proofs by one rule (including the mis-pairing guard). It never
|
|
956
|
+
// throws — a proof fault degrades to "no proof", never to a lost claim.
|
|
957
|
+
const proof = await servableProof(storageRepo, blockId, latest);
|
|
958
|
+
return proof ? { ...latest, proof } : latest;
|
|
875
959
|
} catch {
|
|
876
960
|
return undefined;
|
|
877
961
|
}
|
|
@@ -881,14 +965,11 @@ export async function createLibp2pNodeBase(
|
|
|
881
965
|
// per-peer deadline also bounds a hung request — slowness needs no race here.
|
|
882
966
|
const response = await syncClient.requestBlock({ blockId, rev: undefined });
|
|
883
967
|
if (response.success && response.archive) {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
return { actionId: revisionData.action.actionId, rev: maxRev };
|
|
890
|
-
}
|
|
891
|
-
}
|
|
968
|
+
// Projection lives with the archive shape (`latestClaimFromArchive`) rather than
|
|
969
|
+
// re-read inline here — it reads the claim and its proof out of ONE revision entry,
|
|
970
|
+
// so a serving peer cannot pair a proof with a revision it does not certify.
|
|
971
|
+
const claim = latestClaimFromArchive(response.archive);
|
|
972
|
+
if (claim) return claim;
|
|
892
973
|
}
|
|
893
974
|
// The peer DID answer, without data: `success:false` is the sync service's "Block not
|
|
894
975
|
// found in local storage", and an archive with no usable revisions holds nothing either
|
|
@@ -1065,7 +1146,11 @@ export async function createLibp2pNodeBase(
|
|
|
1065
1146
|
// Setup restoration coordinator with FRET adapter
|
|
1066
1147
|
const restorationCoordinatorV2 = new RestorationCoordinator(
|
|
1067
1148
|
fretAdapter,
|
|
1068
|
-
|
|
1149
|
+
// The node's own IPeerNetwork, not an inline `dialProtocol` lambda: the lambda opened
|
|
1150
|
+
// streams without `runOnLimitedConnection`, so a block holder reachable only through a
|
|
1151
|
+
// relay looked like a peer that simply did not have the block, and it dropped the
|
|
1152
|
+
// caller's AbortSignal, so `SyncClient`'s per-peer dial deadline never bounded a dial.
|
|
1153
|
+
keyNetwork,
|
|
1069
1154
|
`/optimystic/${options.networkName}`,
|
|
1070
1155
|
node.peerId.toString()
|
|
1071
1156
|
);
|
|
@@ -1135,6 +1220,18 @@ export async function createLibp2pNodeBase(
|
|
|
1135
1220
|
// async loop. Adding/deleting a Set entry during iteration does not throw in JS — entries
|
|
1136
1221
|
// are visited best-effort — which is acceptable for a resilience mechanism, so we
|
|
1137
1222
|
// document it here rather than add locking.
|
|
1223
|
+
//
|
|
1224
|
+
// The event's `grown` arm (blocks this node KEEPS whose cohort acquired new peers — the
|
|
1225
|
+
// founder/cohort-growth case): handleRebalanceEvent pushes each grown block to the newly
|
|
1226
|
+
// co-responsible peers and returns a per-block GrowthOutcome in `result.growth`, which is
|
|
1227
|
+
// fed back into the monitor here. The monitor records a peer as seen ONLY off that
|
|
1228
|
+
// feedback (a confirmed replica, or a block that otherwise reached its floor), so a failed
|
|
1229
|
+
// push is re-detected on the next check instead of being silently dropped — bounded by
|
|
1230
|
+
// growthMaxAttempts, after which the monitor abandons the peer for that block (visible in
|
|
1231
|
+
// getGrowthDiagnostics). Nothing is released or untracked off the grown arm — the node
|
|
1232
|
+
// keeps serving the block either way. On the catch path NOTHING is recorded: the monitor's
|
|
1233
|
+
// state stays un-advanced and the next check retries, the correct outcome for a reaction
|
|
1234
|
+
// that threw.
|
|
1138
1235
|
rebalanceMonitor.onRebalance((event) => {
|
|
1139
1236
|
for (const blockId of event.gained) ownedBlocks.add(blockId);
|
|
1140
1237
|
coordinator.handleRebalanceEvent(event).then((result) => {
|
|
@@ -1142,7 +1239,25 @@ export async function createLibp2pNodeBase(
|
|
|
1142
1239
|
rebalanceMonitor.untrackBlock(blockId); // also evicts from the shared ownedBlocks set
|
|
1143
1240
|
gcEligible.add(blockId); // confirmed replicated → safe to sweep
|
|
1144
1241
|
}
|
|
1242
|
+
for (const [blockId, outcome] of result.growth) {
|
|
1243
|
+
rebalanceMonitor.recordGrowthOutcome(blockId, outcome);
|
|
1244
|
+
}
|
|
1245
|
+
if (result.underReplicated.length > 0) {
|
|
1246
|
+
const growthDiag = rebalanceMonitor.getGrowthDiagnostics();
|
|
1247
|
+
log?.('cohort-growth: %d of %d grown blocks not confirmed on new peers this pass ' +
|
|
1248
|
+
'(awaiting-confirmation=%d given-up-pairs=%d)',
|
|
1249
|
+
result.underReplicated.length, event.grown.size,
|
|
1250
|
+
growthDiag.blocksAwaitingConfirmation, growthDiag.abandonedPairs);
|
|
1251
|
+
}
|
|
1145
1252
|
}).catch((err) => {
|
|
1253
|
+
// NOTE: accepted tradeoff — recording nothing here means growthMaxAttempts (which
|
|
1254
|
+
// only counts RECORDED incomplete outcomes) never bounds a reaction that throws
|
|
1255
|
+
// every time, so the re-check timer retries it at growthRecheckIntervalMs forever,
|
|
1256
|
+
// logging each failure. Kept deliberately: per-peer errors are already caught
|
|
1257
|
+
// inside the coordinator, so a throw out of handleRebalanceEvent is a coding bug,
|
|
1258
|
+
// and a loud unbounded retry is the right way to surface one — silently abandoning
|
|
1259
|
+
// the block would hide it and leave the block singly held. Revisit if a legitimate
|
|
1260
|
+
// recoverable condition is ever allowed to throw out of the reaction.
|
|
1146
1261
|
log?.('rebalance reaction failed: %o', err);
|
|
1147
1262
|
});
|
|
1148
1263
|
});
|
|
@@ -1503,13 +1618,13 @@ export async function createLibp2pNodeBase(
|
|
|
1503
1618
|
|
|
1504
1619
|
// Inbound notify frames → forwarder host (subscriber role delivers in-process; forwarder role fans out).
|
|
1505
1620
|
// NOTE: the four `register*Handler` helpers below (notify / pushStateGossip / recover /
|
|
1506
|
-
// matchmaking query) all call `
|
|
1621
|
+
// matchmaking query) all call `registerProtocolHandler(...)` fire-and-forget (`void`), so a rejected
|
|
1507
1622
|
// registration escapes the post-start rollback `catch` as an UNHANDLED rejection instead of
|
|
1508
1623
|
// failing node creation. Harmless today — every protocol id here is a fixed constant registered
|
|
1509
1624
|
// exactly once, so the only realistic rejection is a duplicate, and that needs a caller to pass
|
|
1510
1625
|
// overlapping custom `cohortTopic.host.protocols`. If any of these ids ever becomes
|
|
1511
1626
|
// caller-configurable, or a helper grows a registration that can genuinely fail, make them await
|
|
1512
|
-
// their `
|
|
1627
|
+
// their `registerProtocolHandler` so the failure reaches the rollback.
|
|
1513
1628
|
registerNotifyHandler(node, reactivityProtocols.notify, notify);
|
|
1514
1629
|
offInboundNotify = notify.onNotification((from, n): void => { void forwarderHost.onInbound(from, n); });
|
|
1515
1630
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single place in `db-p2p` that opens a libp2p protocol stream.
|
|
3
|
+
*
|
|
4
|
+
* libp2p refuses to open a protocol stream over a *limited* (circuit-relay) connection unless the
|
|
5
|
+
* caller opts in with `runOnLimitedConnection: true`. Omitting it produces no compile error and no
|
|
6
|
+
* obvious failure — only "that peer never answers" — so every peer reachable only through a relay
|
|
7
|
+
* (the steady state for browsers, phones, and NATed peers) silently drops out. The flag is
|
|
8
|
+
* deliberately NOT an option here: a new call site gets relay support without its author needing to
|
|
9
|
+
* know the flag exists, and `test/dial-options-single-site.spec.ts` fails the build if a second
|
|
10
|
+
* source file calls `dialProtocol` / `newStream` directly.
|
|
11
|
+
*
|
|
12
|
+
* NOTE: accepted tradeoff — this duplicates FRET's `rpc/protocols.ts#openRpcStream` (exported since
|
|
13
|
+
* `p2p-fret@1.0.0-beta.3`) rather than delegating to it. FRET's helper pins `negotiateFully: false`
|
|
14
|
+
* with no way to opt out, and `cohort-topic/stream-util.ts` deliberately does not set that option
|
|
15
|
+
* (see the NOTE at its call sites); adopting FRET's would silently reverse a decision a human
|
|
16
|
+
* already made. It also has no hook for the pre-dial check `libp2p-key-network.ts#connect` runs.
|
|
17
|
+
* Revisit — and delete this module in favour of `openRpcStream` — if FRET ever parameterizes
|
|
18
|
+
* `negotiateFully`.
|
|
19
|
+
*
|
|
20
|
+
* Imports here are type-only on purpose: this module is reachable from the react-native entry
|
|
21
|
+
* (`src/rn.ts`) via `libp2p-key-network.ts`, which must not pull node-specific code.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { Libp2p } from "libp2p";
|
|
25
|
+
import type { Connection, PeerId, Stream } from "@libp2p/interface";
|
|
26
|
+
|
|
27
|
+
export interface OpenProtocolStreamOptions {
|
|
28
|
+
/** Forwarded to libp2p so a caller's deadline cancels both the connection reuse and the dial. */
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
/**
|
|
31
|
+
* Omit for libp2p's default (full multistream-select negotiation at stream-open).
|
|
32
|
+
* Pass `false` to save the round trip, accepting that an unsupported-protocol failure is
|
|
33
|
+
* deferred to the first read — only safe when the caller always reads a reply.
|
|
34
|
+
*/
|
|
35
|
+
negotiateFully?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Runs immediately before a FRESH dial, and never on the connection-reuse path.
|
|
38
|
+
* Throwing aborts the open. This is the seam for checks that are only meaningful when no
|
|
39
|
+
* connection exists yet (see `libp2p-key-network.ts#assertNotSelfRelayOnly`).
|
|
40
|
+
*/
|
|
41
|
+
beforeDial?: () => Promise<void> | void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* True for a circuit-relay ("limited") connection: libp2p stamps one with per-circuit `limits`
|
|
46
|
+
* (data/duration caps); sniffing `/p2p-circuit` in the remote multiaddr covers transports and
|
|
47
|
+
* versions that leave `limits` unpopulated.
|
|
48
|
+
*/
|
|
49
|
+
export function isLimitedConnection(c: Connection): boolean {
|
|
50
|
+
if (c.limits != null) return true;
|
|
51
|
+
return c.remoteAddr?.toString?.().includes("/p2p-circuit") ?? false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Open `protocol` to `peer`, reusing a healthy existing connection when there is one.
|
|
56
|
+
*
|
|
57
|
+
* Skips connections libp2p has not yet evicted from its index but that are no longer open, and
|
|
58
|
+
* prefers a direct connection over a relayed one — a relayed connection can be reset once the
|
|
59
|
+
* relay's per-circuit cap or reservation lapses, and after DCUtR upgrades a link to direct both
|
|
60
|
+
* briefly coexist. Falls back to the relayed connection when it is the only open path.
|
|
61
|
+
*/
|
|
62
|
+
export async function openProtocolStream(
|
|
63
|
+
node: Libp2p,
|
|
64
|
+
peer: PeerId,
|
|
65
|
+
protocol: string,
|
|
66
|
+
options?: OpenProtocolStreamOptions,
|
|
67
|
+
): Promise<Stream> {
|
|
68
|
+
// Before touching connections: a caller that has already given up is owed its own reason,
|
|
69
|
+
// not whatever libp2p would report several layers down.
|
|
70
|
+
options?.signal?.throwIfAborted();
|
|
71
|
+
|
|
72
|
+
const conns = node.getConnections?.(peer) ?? [];
|
|
73
|
+
const open = conns.filter(c => c?.status === "open" && typeof c?.newStream === "function");
|
|
74
|
+
const chosen = open.find(c => !isLimitedConnection(c)) ?? open[0];
|
|
75
|
+
|
|
76
|
+
// Keys are OMITTED rather than set to `undefined` when the caller did not supply them, so
|
|
77
|
+
// libp2p applies its own defaults instead of seeing an explicit `undefined`.
|
|
78
|
+
const streamOptions = {
|
|
79
|
+
runOnLimitedConnection: true,
|
|
80
|
+
...(options?.negotiateFully !== undefined ? { negotiateFully: options.negotiateFully } : {}),
|
|
81
|
+
...(options?.signal !== undefined ? { signal: options.signal } : {}),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (chosen) return await chosen.newStream([protocol], streamOptions);
|
|
85
|
+
await options?.beforeDial?.();
|
|
86
|
+
return await node.dialProtocol(peer, [protocol], streamOptions);
|
|
87
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single place in `db-p2p` that registers a libp2p protocol handler.
|
|
3
|
+
*
|
|
4
|
+
* The mirror image of `./open-protocol-stream.ts`. Opening a stream over a *limited* (circuit-relay)
|
|
5
|
+
* connection needs BOTH sides to opt in, and libp2p checks each side against its own options:
|
|
6
|
+
*
|
|
7
|
+
* - the dialer's `newStream` / `dialProtocol` options (`libp2p/src/connection.ts`, outbound path), and
|
|
8
|
+
* - the options the *answering* peer passed when it registered the handler
|
|
9
|
+
* (`libp2p/src/connection.ts`, incoming path — it reads back
|
|
10
|
+
* `registrar.getHandler(protocol).options.runOnLimitedConnection`).
|
|
11
|
+
*
|
|
12
|
+
* Miss either one and libp2p throws `LimitedConnectionError`. On the answering side it does so
|
|
13
|
+
* *after* multistream-select has already acknowledged the protocol, so the dialer's stream open
|
|
14
|
+
* appears to succeed and the stream is then reset with no reply — indistinguishable, from the
|
|
15
|
+
* dialer's seat, from a peer that simply does not hold the data. Every peer reachable only through
|
|
16
|
+
* a capped relay (phones, browsers, machines behind a home router) silently drops out.
|
|
17
|
+
*
|
|
18
|
+
* `openProtocolStream` fixed the dialling half; this fixes the answering half. As there, the flag is
|
|
19
|
+
* deliberately NOT an option: a new protocol gets relay support without its author needing to know
|
|
20
|
+
* the flag exists, and `test/dial-options-single-site.spec.ts` fails the build if a second source
|
|
21
|
+
* file calls `.handle(...)` directly.
|
|
22
|
+
*
|
|
23
|
+
* Accepting the stream is necessary but not sufficient. A stock relay also caps the *whole* relayed
|
|
24
|
+
* connection — `applyDefaultLimit: true` stamps `Limit { data: 128 KiB, duration: 2 min }` on every
|
|
25
|
+
* reservation — so a relay-only peer now answers, but the circuit is reset once either cap is hit,
|
|
26
|
+
* which one cohort frame (`DEFAULT_STREAM_MAX_BYTES`, 512 KiB) can do on its own. Lifting that is
|
|
27
|
+
* the relay operator's knob, not this one: see `NodeOptions.relayServerInit` in `libp2p-node-base.ts`.
|
|
28
|
+
*
|
|
29
|
+
* Imports here are type-only on purpose: this module is reachable from the react-native entry
|
|
30
|
+
* (`src/rn.ts`), which must not pull node-specific code.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { StreamHandler, StreamHandlerOptions, StreamMiddleware } from "@libp2p/interface";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Anything that can register a libp2p protocol handler.
|
|
37
|
+
*
|
|
38
|
+
* Both shapes in this package satisfy it: a `Libp2p` node (`node.handle(...)`) and the `registrar`
|
|
39
|
+
* component the service classes are constructed with (`components.registrar.handle(...)`).
|
|
40
|
+
*/
|
|
41
|
+
export interface ProtocolRegistrar {
|
|
42
|
+
handle(protocol: string, handler: StreamHandler, options?: StreamHandlerOptions): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The per-protocol settings that legitimately vary. `runOnLimitedConnection` is absent on purpose —
|
|
47
|
+
* it is a constant of this helper, not a caller's choice.
|
|
48
|
+
*/
|
|
49
|
+
export interface RegisterProtocolHandlerOptions {
|
|
50
|
+
/** Concurrent inbound streams allowed per connection. Omit for libp2p's default (32). */
|
|
51
|
+
maxInboundStreams?: number;
|
|
52
|
+
/** Concurrent outbound streams allowed per connection. Omit for libp2p's default (64). */
|
|
53
|
+
maxOutboundStreams?: number;
|
|
54
|
+
/** Stream middleware, run around the handler. Omit for none. */
|
|
55
|
+
middleware?: StreamMiddleware[];
|
|
56
|
+
/** Replace an existing registration for this protocol instead of rejecting. */
|
|
57
|
+
force?: true;
|
|
58
|
+
/** Forwarded to libp2p so a caller's deadline cancels the registration. */
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register `handler` for `protocol`, accepting streams over limited (relay) connections.
|
|
64
|
+
*
|
|
65
|
+
* Rejects if the registrar rejects — a duplicate protocol id, or an aborted signal. Callers that
|
|
66
|
+
* register fire-and-forget (`void registerProtocolHandler(...)`) keep that behaviour; see the NOTE
|
|
67
|
+
* in `libp2p-node-base.ts` about the reactivity handlers.
|
|
68
|
+
*/
|
|
69
|
+
export async function registerProtocolHandler(
|
|
70
|
+
target: ProtocolRegistrar,
|
|
71
|
+
protocol: string,
|
|
72
|
+
handler: StreamHandler,
|
|
73
|
+
options?: RegisterProtocolHandlerOptions,
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
// Keys are OMITTED rather than set to `undefined` when the caller did not supply them, so
|
|
76
|
+
// libp2p applies its own defaults instead of seeing an explicit `undefined`.
|
|
77
|
+
await target.handle(protocol, handler, {
|
|
78
|
+
runOnLimitedConnection: true,
|
|
79
|
+
...(options?.maxInboundStreams !== undefined ? { maxInboundStreams: options.maxInboundStreams } : {}),
|
|
80
|
+
...(options?.maxOutboundStreams !== undefined ? { maxOutboundStreams: options.maxOutboundStreams } : {}),
|
|
81
|
+
...(options?.middleware !== undefined ? { middleware: options.middleware } : {}),
|
|
82
|
+
...(options?.force !== undefined ? { force: options.force } : {}),
|
|
83
|
+
...(options?.signal !== undefined ? { signal: options.signal } : {}),
|
|
84
|
+
});
|
|
85
|
+
}
|