@optimystic/db-p2p 0.13.5 → 0.14.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 +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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — participant-side bootstrap-evidence builder (db-p2p side of the `buildBootstrapEvidence` seam).
|
|
3
|
+
*
|
|
4
|
+
* Implements the db-core `CohortTopicServiceDeps.buildBootstrapEvidence` seam for the node's participant
|
|
5
|
+
* role: on a cold-start `bootstrap: true` re-issue the service calls this with the register's own
|
|
6
|
+
* canonical `(topicId, tier, participantCoord, timestamp)` tuple (base64url wire strings) and attaches the
|
|
7
|
+
* returned bytes — **before** signing — into `RegisterV1.bootstrapEvidence`.
|
|
8
|
+
*
|
|
9
|
+
* - **Tier ≤ maxNoPowTier (T0/T1):** proof-of-work is not the expected evidence. With an `endorse`
|
|
10
|
+
* capability (a key-ful node) we mint a *self-vouch reputation endorsement* over the bound image so a
|
|
11
|
+
* configured cohort's referee verifier admits it; without one we return `undefined` (parent-reference
|
|
12
|
+
* origination is the follow-on `cohort-topic-bootstrap-parent-reference`). A T0/T1 bootstrap with no
|
|
13
|
+
* evidence is denied by a configured cohort until that lands — but the single-tier-0 milestone's
|
|
14
|
+
* cohort-side tests construct evidence directly, so this does not block them.
|
|
15
|
+
* - **Tier ≥ maxNoPowTier+1 (T2/T3):** mint a proof-of-work — loop nonces until
|
|
16
|
+
* `meetsDifficulty(hash.H(powPreimage(reg, nonce)), bits)`. Bound work ≈ `2^bits` hashes (default 20 ≈
|
|
17
|
+
* ~1 M, sub-second). The loop is capped ({@link DEFAULT_POW_MAX_ITERATIONS}); on cap-exceeded it returns
|
|
18
|
+
* `undefined` so the register proceeds without evidence rather than ever hanging the register path.
|
|
19
|
+
*
|
|
20
|
+
* Returns the **raw** envelope JSON bytes (`utf8(JSON.stringify(env))`), NOT the already-base64url string
|
|
21
|
+
* from `serializeBootstrapEvidenceEnvelope` — the service base64url-encodes them itself (returning the
|
|
22
|
+
* serialized string's bytes would double-encode the field). We obtain the raw bytes by decoding the
|
|
23
|
+
* canonical serializer output once, so the byte layout matches exactly what a verifier reconstructs.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
serializeBootstrapEvidenceEnvelope,
|
|
28
|
+
bootstrapBoundImage,
|
|
29
|
+
powPreimage,
|
|
30
|
+
meetsDifficulty,
|
|
31
|
+
DEFAULT_POW_DIFFICULTY_BITS,
|
|
32
|
+
DEFAULT_MAX_NO_POW_TIER,
|
|
33
|
+
bytesToB64url,
|
|
34
|
+
b64urlToBytes,
|
|
35
|
+
type IRingHash,
|
|
36
|
+
type BootstrapBoundFields,
|
|
37
|
+
type BootstrapEvidenceEnvelopeV1,
|
|
38
|
+
type ReputationEvidenceV1,
|
|
39
|
+
} from "@optimystic/db-core";
|
|
40
|
+
import { randomBytes } from "@libp2p/crypto";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Defensive cap on the PoW nonce search (~16.7 M hashes). Comfortably above the ~`2^bits` expected for
|
|
44
|
+
* the default 20-bit difficulty (~1 M), so a real miner solves long before the cap; hitting it (a
|
|
45
|
+
* mis-set, very-high `bits`) returns `undefined` rather than hanging the register path.
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_POW_MAX_ITERATIONS = 1 << 24;
|
|
48
|
+
|
|
49
|
+
/** The bound tuple the service hands the builder — the same shape a verifier binds via {@link bootstrapBoundImage}. */
|
|
50
|
+
export type BootstrapEvidenceBuildParams = BootstrapBoundFields;
|
|
51
|
+
|
|
52
|
+
/** Inputs to {@link createBootstrapEvidenceBuilder}. */
|
|
53
|
+
export interface BootstrapEvidenceBuilderDeps {
|
|
54
|
+
/** The node's ring hash (SHA-256) — the same `H` the PoW verifier checks against. */
|
|
55
|
+
readonly hash: IRingHash;
|
|
56
|
+
/** Difficulty bits to mint at. Default {@link DEFAULT_POW_DIFFICULTY_BITS}. `0` solves on the first nonce (test). */
|
|
57
|
+
readonly bits?: number;
|
|
58
|
+
/** Highest tier exempt from PoW (T0/T1 → 1). Default {@link DEFAULT_MAX_NO_POW_TIER}. */
|
|
59
|
+
readonly maxNoPowTier?: number;
|
|
60
|
+
/** Nonce-search cap. Default {@link DEFAULT_POW_MAX_ITERATIONS}. */
|
|
61
|
+
readonly maxIterations?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Optional self-vouch endorsement capability for a key-ful node: signs the bound image with the node's
|
|
64
|
+
* peer key and returns the referee (= self) + signature. Supplied → T0/T1 mints a reputation
|
|
65
|
+
* endorsement (the interim T0/T1 path until parent-reference origination lands); absent → T0/T1 carries
|
|
66
|
+
* no evidence.
|
|
67
|
+
*/
|
|
68
|
+
readonly endorse?: (boundImage: Uint8Array) => Promise<ReputationEvidenceV1>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build the {@link import("@optimystic/db-core").CohortTopicServiceDeps.buildBootstrapEvidence} seam:
|
|
73
|
+
* a `(params) => Promise<Uint8Array | undefined>` that mints the cold-start evidence for the node's own
|
|
74
|
+
* register. PoW for T2/T3; a self-vouch reputation endorsement (when `endorse` is supplied) or nothing
|
|
75
|
+
* for T0/T1. Never throws and never hangs (the nonce loop is capped).
|
|
76
|
+
*/
|
|
77
|
+
export function createBootstrapEvidenceBuilder(
|
|
78
|
+
deps: BootstrapEvidenceBuilderDeps,
|
|
79
|
+
): (params: BootstrapEvidenceBuildParams) => Promise<Uint8Array | undefined> {
|
|
80
|
+
const bits = deps.bits ?? DEFAULT_POW_DIFFICULTY_BITS;
|
|
81
|
+
const maxNoPowTier = deps.maxNoPowTier ?? DEFAULT_MAX_NO_POW_TIER;
|
|
82
|
+
const maxIterations = deps.maxIterations ?? DEFAULT_POW_MAX_ITERATIONS;
|
|
83
|
+
|
|
84
|
+
return async (params: BootstrapEvidenceBuildParams): Promise<Uint8Array | undefined> => {
|
|
85
|
+
const bound: BootstrapBoundFields = {
|
|
86
|
+
topicId: params.topicId,
|
|
87
|
+
tier: params.tier,
|
|
88
|
+
participantCoord: params.participantCoord,
|
|
89
|
+
timestamp: params.timestamp,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (params.tier <= maxNoPowTier) {
|
|
93
|
+
// T0/T1: PoW is not the expected evidence. A key-ful node self-vouches; otherwise no evidence
|
|
94
|
+
// (the parent-reference path is the follow-on ticket — documented deferral).
|
|
95
|
+
if (deps.endorse === undefined) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const reputation = await deps.endorse(bootstrapBoundImage(bound));
|
|
99
|
+
return rawEnvelopeBytes({ v: 1, reputation });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// T2/T3: mint a proof-of-work — search nonces until the digest meets the difficulty target.
|
|
103
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
104
|
+
const nonce = randomBytes(16); // CSPRNG nonce — bound to the register tuple via the preimage
|
|
105
|
+
if (meetsDifficulty(deps.hash.H(powPreimage(bound, nonce)), bits)) {
|
|
106
|
+
return rawEnvelopeBytes({ v: 1, pow: { nonce: bytesToB64url(nonce) } });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Cap exceeded (mis-set difficulty): attach nothing rather than hang. A configured cohort denies it.
|
|
110
|
+
return undefined;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The raw envelope JSON bytes the service expects (`utf8(JSON.stringify(env))`). Reuses the db-core
|
|
116
|
+
* canonical serializer (fixed field order, deterministic JSON) and decodes its base64url output back to
|
|
117
|
+
* the underlying bytes — so the layout is byte-identical to what a verifier reconstructs, with zero
|
|
118
|
+
* duplicated canonicalization here.
|
|
119
|
+
*/
|
|
120
|
+
function rawEnvelopeBytes(env: BootstrapEvidenceEnvelopeV1): Uint8Array {
|
|
121
|
+
return b64urlToBytes(serializeBootstrapEvidenceEnvelope(env));
|
|
122
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — real bootstrap-evidence verifiers (db-p2p side of the anti-DoS gate).
|
|
3
|
+
*
|
|
4
|
+
* db-core owns the *policy* (which evidence kinds satisfy which tier) and the crypto-free envelope /
|
|
5
|
+
* bound-image / PoW-preimage helpers (`antidos/bootstrap-evidence-envelope.ts`); it deliberately embeds
|
|
6
|
+
* no specific PoW or reputation scheme. This module supplies the **self-contained cryptographic checks**
|
|
7
|
+
* the host injects into `createBootstrapEvidence`, binding the node's `RingHash` (the same SHA-256 the
|
|
8
|
+
* addressing uses) and the cohort-topic peer-key `verifyPeerSig` — no new crypto dependency.
|
|
9
|
+
*
|
|
10
|
+
* Both verifiers are **synchronous** (one hash, or one signature verify + two reputation-map reads) and
|
|
11
|
+
* **total** — any parse / decode / verify failure on attacker-supplied input yields `false`, never a
|
|
12
|
+
* throw — because `BootstrapEvidence.verify` runs inside `member-engine.ts`'s `runGuards` on every
|
|
13
|
+
* register and must do no network I/O (that would itself be a DoS amplifier).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
parseBootstrapEvidenceEnvelope,
|
|
18
|
+
powPreimage,
|
|
19
|
+
bootstrapBoundImage,
|
|
20
|
+
meetsDifficulty,
|
|
21
|
+
DEFAULT_POW_DIFFICULTY_BITS,
|
|
22
|
+
b64urlToBytes,
|
|
23
|
+
type IRingHash,
|
|
24
|
+
type RegisterV1,
|
|
25
|
+
} from "@optimystic/db-core";
|
|
26
|
+
import { DEFAULT_THRESHOLDS } from "../reputation/types.js";
|
|
27
|
+
import { verifyPeerSig } from "./peer-sig.js";
|
|
28
|
+
import { bytesToPeerIdString } from "./peer-codec.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The slice of a peer-reputation service the bootstrap-evidence referee verifier consults — a subset of
|
|
32
|
+
* {@link import("../reputation/types.js").IPeerReputation} that `PeerReputationService` satisfies
|
|
33
|
+
* directly. "Sufficient reputation" is **stronger than mere non-ban**: the referee must be *both* not
|
|
34
|
+
* banned *and* below the deprioritize threshold.
|
|
35
|
+
*/
|
|
36
|
+
export interface BootstrapReputationView {
|
|
37
|
+
/** True when `peerId` (a peer-id string) is banned / excluded from operations. */
|
|
38
|
+
isBanned(peerId: string): boolean;
|
|
39
|
+
/** Effective reputation score for `peerId` (0 = a clean, unseen peer). Lower is better. */
|
|
40
|
+
getScore(peerId: string): number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The default "sufficient reputation" cutoff: a referee with `score < deprioritize` is reputable enough
|
|
45
|
+
* to endorse a bootstrap. Mirrors the reputation service's default `deprioritize` threshold (20) so a
|
|
46
|
+
* default-configured node and the gate agree on "sufficient". Strict `<`, so a referee *at* the
|
|
47
|
+
* threshold is not sufficient.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_DEPRIORITIZE_THRESHOLD = DEFAULT_THRESHOLDS.deprioritize;
|
|
50
|
+
|
|
51
|
+
/** Inputs to {@link createPoWVerifier}. */
|
|
52
|
+
export interface PoWVerifierDeps {
|
|
53
|
+
/** The node's ring hash (SHA-256) — the same `H` the addressing uses; hashes the PoW preimage. */
|
|
54
|
+
readonly hash: IRingHash;
|
|
55
|
+
/** Required leading-zero bits. Default {@link DEFAULT_POW_DIFFICULTY_BITS}. `0` admits any nonce (test). */
|
|
56
|
+
readonly bits?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A real proof-of-work verifier for the T2/T3 evidence path. Self-contained — no subsystem, one hash.
|
|
61
|
+
*
|
|
62
|
+
* ```
|
|
63
|
+
* env = parseBootstrapEvidenceEnvelope(reg); if (!env?.pow) return false
|
|
64
|
+
* h = hash.H(powPreimage(reg, b64urlToBytes(env.pow.nonce)))
|
|
65
|
+
* return meetsDifficulty(h, bits)
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* The preimage binds `(topicId, tier, participantCoord, timestamp)` (via {@link bootstrapBoundImage}),
|
|
69
|
+
* so a PoW minted for one topic / peer / time cannot bootstrap another. Any absent `pow`, malformed
|
|
70
|
+
* envelope, or non-base64url nonce → `false` (fails closed).
|
|
71
|
+
*/
|
|
72
|
+
export function createPoWVerifier(deps: PoWVerifierDeps): (reg: RegisterV1) => boolean {
|
|
73
|
+
const bits = deps.bits ?? DEFAULT_POW_DIFFICULTY_BITS;
|
|
74
|
+
return (reg: RegisterV1): boolean => {
|
|
75
|
+
try {
|
|
76
|
+
const env = parseBootstrapEvidenceEnvelope(reg);
|
|
77
|
+
if (env?.pow === undefined) {
|
|
78
|
+
return false; // not offered / not a PoW envelope
|
|
79
|
+
}
|
|
80
|
+
const nonce = b64urlToBytes(env.pow.nonce);
|
|
81
|
+
return meetsDifficulty(deps.hash.H(powPreimage(reg, nonce)), bits);
|
|
82
|
+
} catch {
|
|
83
|
+
return false; // any decode / hash failure on attacker input → fail closed
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Inputs to {@link createReputationVerifier}. */
|
|
89
|
+
export interface ReputationVerifierDeps {
|
|
90
|
+
/** Local reputation view the referee is scored against (not banned + below the threshold). */
|
|
91
|
+
readonly reputation: BootstrapReputationView;
|
|
92
|
+
/** Strict "sufficient reputation" cutoff. Default {@link DEFAULT_DEPRIORITIZE_THRESHOLD}. */
|
|
93
|
+
readonly deprioritizeThreshold?: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A real reputation-endorsement verifier: a *referee* peer endorses the bootstrap by peer-key-signing
|
|
98
|
+
* the {@link bootstrapBoundImage}, and the cohort checks the signature **and** that the referee is
|
|
99
|
+
* sufficiently reputable in the **local** reputation view.
|
|
100
|
+
*
|
|
101
|
+
* ```
|
|
102
|
+
* env = parseBootstrapEvidenceEnvelope(reg); if (!env?.reputation) return false
|
|
103
|
+
* refereeBytes = b64urlToBytes(env.reputation.referee)
|
|
104
|
+
* if (!verifyPeerSig(refereeBytes, bootstrapBoundImage(reg), b64urlToBytes(env.reputation.sig))) return false
|
|
105
|
+
* refereeId = bytesToPeerIdString(refereeBytes)
|
|
106
|
+
* return !reputation.isBanned(refereeId) && reputation.getScore(refereeId) < deprioritizeThreshold
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* The `referee` MAY equal the participant (a reputable participant self-vouches with its own peer key).
|
|
110
|
+
* An unknown referee scores `0` (a clean, unseen peer) and so is sufficient; a referee exactly *at* the
|
|
111
|
+
* threshold is not (strict `<`). Bad sig / banned / over-threshold / malformed → `false` (fails closed).
|
|
112
|
+
*/
|
|
113
|
+
export function createReputationVerifier(deps: ReputationVerifierDeps): (reg: RegisterV1) => boolean {
|
|
114
|
+
const threshold = deps.deprioritizeThreshold ?? DEFAULT_DEPRIORITIZE_THRESHOLD;
|
|
115
|
+
return (reg: RegisterV1): boolean => {
|
|
116
|
+
try {
|
|
117
|
+
const env = parseBootstrapEvidenceEnvelope(reg);
|
|
118
|
+
if (env?.reputation === undefined) {
|
|
119
|
+
return false; // not offered / not a reputation envelope
|
|
120
|
+
}
|
|
121
|
+
const refereeBytes = b64urlToBytes(env.reputation.referee);
|
|
122
|
+
const sig = b64urlToBytes(env.reputation.sig);
|
|
123
|
+
if (!verifyPeerSig(refereeBytes, bootstrapBoundImage(reg), sig)) {
|
|
124
|
+
return false; // signature does not bind this register's tuple to the referee key
|
|
125
|
+
}
|
|
126
|
+
const refereeId = bytesToPeerIdString(refereeBytes);
|
|
127
|
+
return !deps.reputation.isBanned(refereeId) && deps.reputation.getScore(refereeId) < threshold;
|
|
128
|
+
} catch {
|
|
129
|
+
return false; // any decode / verify failure on attacker input → fail closed
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — real signed-parent-reference bootstrap-evidence verifier (db-p2p side).
|
|
3
|
+
*
|
|
4
|
+
* The committed-work proxy for the no-proof-of-work path: a `bootstrap: true` register carries a **signed
|
|
5
|
+
* reference to a parent topic that actually exists**, and the cohort admits it only after confirming the
|
|
6
|
+
* parent topic exists in locally-available committed / membership state. This is the *only* accepted
|
|
7
|
+
* evidence for the committed tiers (T0/T1) and the third option for T2/T3 (`PoW || reputation || parent-ref`).
|
|
8
|
+
*
|
|
9
|
+
* It replaces the interim reputation stand-in for `verifyParentReference` that
|
|
10
|
+
* `cohort-topic-bootstrap-evidence-verifiers` left in `host.ts`.
|
|
11
|
+
*
|
|
12
|
+
* The verifier is **synchronous** (it runs in `member-engine.ts`'s `runGuards` on every register) and
|
|
13
|
+
* **total** (any parse / decode / verify failure on attacker input yields `false`, never a throw). The
|
|
14
|
+
* existence check therefore consults a **synchronous local view** — never a network fetch, which inside an
|
|
15
|
+
* admission gate would itself be a DoS amplifier.
|
|
16
|
+
*
|
|
17
|
+
* Two independent checks, both must pass (`docs/cohort-topic.md` §Anti-DoS bullet 4):
|
|
18
|
+
*
|
|
19
|
+
* 1. **Signed reference (anti-replay).** The participant peer-key-signs the {@link parentRefSigningImage}
|
|
20
|
+
* — the bound tuple extended with `parentTopicId` — so a reference minted for one
|
|
21
|
+
* `(topic, tier, peer, time, parent)` cannot be lifted onto another register. Verified against the
|
|
22
|
+
* participant's own peer key, so it stands alone even in key-less mode (where the outer register
|
|
23
|
+
* signature is absent).
|
|
24
|
+
* 2. **Existence.** The referenced parent topic must exist in locally-available committed / membership
|
|
25
|
+
* state, via the injectable synchronous {@link BootstrapParentTopicView}.
|
|
26
|
+
*
|
|
27
|
+
* A node only admits a parent-ref bootstrap for a parent topic it has *locally cached* a cert/commit for
|
|
28
|
+
* (fail-closed when unknown). That is acceptable for an admission gate: a participant whose parent the node
|
|
29
|
+
* does not know retries / uses PoW (T2/T3); a genuinely-new committed (T0/T1) topic is bootstrapped by
|
|
30
|
+
* nodes that already serve the parent's committed work, which hold its cert. A *richer* check — that the
|
|
31
|
+
* parent's commit certificate names *this* child topic — is the follow-on
|
|
32
|
+
* `cohort-topic-parent-ref-tx-log-content`, not this module.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
parseBootstrapEvidenceEnvelope,
|
|
37
|
+
parentRefSigningImage,
|
|
38
|
+
DEFAULT_MAX_NO_POW_TIER,
|
|
39
|
+
b64urlToBytes,
|
|
40
|
+
type RegisterV1,
|
|
41
|
+
type RingCoord,
|
|
42
|
+
} from "@optimystic/db-core";
|
|
43
|
+
import { verifyPeerSig } from "./peer-sig.js";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A synchronous local view answering "does this node locally know parent topic `parentTopicId` exists?".
|
|
47
|
+
* Backed (in the host default) by the committed / FRET membership state the node already holds — never a
|
|
48
|
+
* network lookup (a round-trip inside an admission gate is a DoS amplifier).
|
|
49
|
+
*/
|
|
50
|
+
export interface BootstrapParentTopicView {
|
|
51
|
+
/**
|
|
52
|
+
* True iff the node locally knows parent topic `parentTopicId` exists (a committed cohort serves it /
|
|
53
|
+
* its membership cert is cached) for a cohort at `tier`. `tier` is the registering child's tier and
|
|
54
|
+
* routes the lookup to the committed (T0/T1) or FRET (T2/T3) backing, mirroring the membership source.
|
|
55
|
+
*/
|
|
56
|
+
exists(parentTopicId: Uint8Array, tier: number): boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Inputs to {@link createParentReferenceVerifier}. */
|
|
60
|
+
export interface ParentReferenceVerifierDeps {
|
|
61
|
+
/** The synchronous local existence view the verifier consults (no network I/O). */
|
|
62
|
+
readonly parentTopicView: BootstrapParentTopicView;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The real, all-tiers `verifyParentReference(reg) => boolean`. Total and synchronous:
|
|
67
|
+
*
|
|
68
|
+
* ```
|
|
69
|
+
* env = parseBootstrapEvidenceEnvelope(reg); if (!env?.parentRef) return false
|
|
70
|
+
* if (parentRef.parentTopicId === reg.topicId) return false // a topic cannot vouch for itself
|
|
71
|
+
* if (!verifyPeerSig(participantCoord, parentRefSigningImage(reg, parentTopicId), parentRef.sig)) return false
|
|
72
|
+
* return parentTopicView.exists(parentTopicId, reg.tier)
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* - Absent `parentRef`, a malformed envelope, a bad/non-base64url signature, or an unknown parent → `false`
|
|
76
|
+
* (fails closed → `unwilling_cohort`).
|
|
77
|
+
* - The self-referential guard rejects `parentTopicId === reg.topicId`: a topic cannot reference itself to
|
|
78
|
+
* prove its own existence (a circular bootstrap).
|
|
79
|
+
*/
|
|
80
|
+
export function createParentReferenceVerifier(deps: ParentReferenceVerifierDeps): (reg: RegisterV1) => boolean {
|
|
81
|
+
const { parentTopicView } = deps;
|
|
82
|
+
return (reg: RegisterV1): boolean => {
|
|
83
|
+
try {
|
|
84
|
+
const env = parseBootstrapEvidenceEnvelope(reg);
|
|
85
|
+
if (env?.parentRef === undefined) {
|
|
86
|
+
return false; // not offered / not a parent-reference envelope
|
|
87
|
+
}
|
|
88
|
+
const { parentTopicId, sig } = env.parentRef;
|
|
89
|
+
// A topic cannot vouch for its own existence (self-referential / circular bootstrap).
|
|
90
|
+
if (parentTopicId === reg.topicId) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
// 1. Signed reference (anti-replay): the participant binds THIS parent to THIS register.
|
|
94
|
+
const image = parentRefSigningImage(reg, parentTopicId);
|
|
95
|
+
if (!verifyPeerSig(b64urlToBytes(reg.participantCoord), image, b64urlToBytes(sig))) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
// 2. Existence: the parent topic must exist in locally-available committed / membership state.
|
|
99
|
+
return parentTopicView.exists(b64urlToBytes(parentTopicId), reg.tier);
|
|
100
|
+
} catch {
|
|
101
|
+
return false; // any decode / verify failure on attacker input → fail closed
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A synchronous local "is a cohort cached for this coord?" read — the {@link FretMembershipSource} cache shape. */
|
|
107
|
+
export interface LocalCohortExistence {
|
|
108
|
+
/** True iff a `MembershipCertV1` is locally cached for `coord`. */
|
|
109
|
+
has(coord: RingCoord): boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Inputs to {@link createDefaultParentTopicView}. */
|
|
113
|
+
export interface DefaultParentTopicViewDeps {
|
|
114
|
+
/** The FRET membership cache (T2/T3 backing): a cached cert means a cohort genuinely serves the parent. */
|
|
115
|
+
readonly membershipSource: LocalCohortExistence;
|
|
116
|
+
/** Tier addressing — supplies `coord0(parentTopicId)`, the parent topic's root cohort coordinate. */
|
|
117
|
+
readonly addressing: { coord0(topicId: Uint8Array): RingCoord };
|
|
118
|
+
/**
|
|
119
|
+
* Optional committed-state backing for the committed tiers (T0/T1). Reads `coord_0(parentTopicId)`. When
|
|
120
|
+
* **omitted the committed tiers fail closed** (return `false`) — a FRET-cached cert (T2/T3 membership)
|
|
121
|
+
* must NOT vouch for committed-tier parent existence (committed-tier integrity). A node supplies a real
|
|
122
|
+
* committed-by-coord reader (e.g. a future tx-log / commit-cert index) here when one is available.
|
|
123
|
+
*/
|
|
124
|
+
readonly committedReader?: (coord: RingCoord) => boolean;
|
|
125
|
+
/** Highest tier exempt from PoW / served by committed work (T0/T1 → 1). Default {@link DEFAULT_MAX_NO_POW_TIER}. */
|
|
126
|
+
readonly maxNoPowTier?: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The host-default {@link BootstrapParentTopicView}, tier-routed exactly like the membership source
|
|
131
|
+
* (`docs/cohort-topic.md` §Membership source):
|
|
132
|
+
*
|
|
133
|
+
* - **T0/T1 (committed tiers):** consult `committedReader` — the committed-state backing. Absent → `false`
|
|
134
|
+
* (fail closed). A FRET-cached cert never satisfies a committed-tier existence check, so committed-tier
|
|
135
|
+
* integrity holds: a parent known only as a FRET (T2/T3) cohort cannot back a committed parent reference.
|
|
136
|
+
* - **T2/T3:** `membershipSource.has(coord_0(parentTopicId))` — a cached `MembershipCertV1` means a cohort
|
|
137
|
+
* is genuinely serving the parent topic.
|
|
138
|
+
*
|
|
139
|
+
* **Known limitation (interim).** No coord-keyed committed-membership index exists yet (the transaction-log
|
|
140
|
+
* commit certificate is keyed by action, not by `coord_0`), so a node typically wires no `committedReader`
|
|
141
|
+
* and T0/T1 parent-ref existence fails closed. T2/T3 parent-ref is fully real today. The dedicated committed
|
|
142
|
+
* backing is the follow-on `cohort-topic-parent-ref-tx-log-content`.
|
|
143
|
+
*/
|
|
144
|
+
export function createDefaultParentTopicView(deps: DefaultParentTopicViewDeps): BootstrapParentTopicView {
|
|
145
|
+
const { membershipSource, addressing, committedReader } = deps;
|
|
146
|
+
const maxNoPowTier = deps.maxNoPowTier ?? DEFAULT_MAX_NO_POW_TIER;
|
|
147
|
+
return {
|
|
148
|
+
exists(parentTopicId: Uint8Array, tier: number): boolean {
|
|
149
|
+
const coord = addressing.coord0(parentTopicId);
|
|
150
|
+
if (tier <= maxNoPowTier) {
|
|
151
|
+
// Committed tiers (T0/T1): require a committed backing; fail closed without one. The FRET cache
|
|
152
|
+
// must not vouch for committed-tier existence (committed-tier integrity).
|
|
153
|
+
return committedReader !== undefined && committedReader(coord);
|
|
154
|
+
}
|
|
155
|
+
// T2/T3: the FRET membership cache reflects a cohort serving the parent topic.
|
|
156
|
+
return membershipSource.has(coord);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { CohortTopicService, CollectionChangeEvent, CollectionChangeListener, CommitCert, IBlockChangeNotifier } from "@optimystic/db-core";
|
|
2
|
+
import { createLogger } from "../logger.js";
|
|
3
|
+
|
|
4
|
+
const log = createLogger('cohort-change-bridge');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The local commit feed the bridge observes: a {@link IBlockChangeNotifier} (the node's
|
|
8
|
+
* `StorageRepo`) extended with the catch-all {@link onAnyCollectionChange} the bridge needs to see
|
|
9
|
+
* EVERY commit — it cannot enumerate collection ids ahead of time to subscribe per-collection, and
|
|
10
|
+
* origination must fire whether or not anyone subscribed to that collection.
|
|
11
|
+
*/
|
|
12
|
+
export interface ChangeBridgeSource extends IBlockChangeNotifier {
|
|
13
|
+
onAnyCollectionChange(listener: CollectionChangeListener): () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CohortTopicChangeNotifierDeps {
|
|
17
|
+
/** The local node's commit feed (its `StorageRepo`). */
|
|
18
|
+
readonly source: ChangeBridgeSource;
|
|
19
|
+
/** The cohort-topic substrate whose `onLocalCommit` origination hook receives member commits. */
|
|
20
|
+
readonly service: CohortTopicService;
|
|
21
|
+
/**
|
|
22
|
+
* True iff this node is a cohort member responsible for this change event's reactivity-topic fan-out.
|
|
23
|
+
* Receives the whole {@link CollectionChangeEvent} (not just the collection id) because the reactivity
|
|
24
|
+
* topic is tail-anchored — `H(event.tailId ‖ "reactivity")` — so the gate needs `event.tailId` to
|
|
25
|
+
* derive the topic's `coord_0` cohort. A tail-less event (a read-driven promotion) is never a member.
|
|
26
|
+
*/
|
|
27
|
+
readonly selfIsCohortMember: (event: CollectionChangeEvent) => boolean;
|
|
28
|
+
/** Resolve the pass-through commit cert for a change event (e.g. the cluster commit-cert store). */
|
|
29
|
+
readonly extractCommitCert: (event: CollectionChangeEvent) => CommitCert | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The reactivity/matchmaking **origination point**: bridge the local single-node change-notifier
|
|
34
|
+
* primitive into the networked cohort-topic substrate.
|
|
35
|
+
*
|
|
36
|
+
* On EVERY commit landing on this node (via the catch-all {@link ChangeBridgeSource.onAnyCollectionChange}
|
|
37
|
+
* feed), if this node is a cohort member for the collection's reactivity topic, the bridge hands the
|
|
38
|
+
* `CollectionChangeEvent` plus the pass-through {@link CommitCert} to `service.onLocalCommit` —
|
|
39
|
+
* reactivity reuses the commit cert's threshold signature directly and never re-signs. A commit on a
|
|
40
|
+
* non-member node (no fan-out responsibility) or one for which no cert is retained (nothing
|
|
41
|
+
* authoritative to forward) is a no-op. A throwing downstream hook is isolated + logged so
|
|
42
|
+
* origination can never break the commit (matching the {@link IBlockChangeNotifier} listener contract).
|
|
43
|
+
*
|
|
44
|
+
* The returned value IS an {@link IBlockChangeNotifier}: it is what `network-transactor` takes as its
|
|
45
|
+
* `localChangeNotifier`, so per-collection {@link IBlockChangeNotifier.onCollectionChange} subscribers
|
|
46
|
+
* (e.g. the Quereus reactive-watch vtab) keep working — those subscriptions delegate straight to
|
|
47
|
+
* `source`. Origination runs independently on the catch-all feed.
|
|
48
|
+
*
|
|
49
|
+
* `makeCohortTopicChangeNotifier` discards the catch-all unsubscribe (node-lifetime by intent) for the
|
|
50
|
+
* unit tests; the node assembly uses {@link attachCohortChangeBridge}, which returns the teardown.
|
|
51
|
+
*/
|
|
52
|
+
export function makeCohortTopicChangeNotifier(deps: CohortTopicChangeNotifierDeps): IBlockChangeNotifier {
|
|
53
|
+
return buildCohortTopicChangeBridge(deps).notifier;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Shared wiring behind both entry points: subscribe the origination handler to the source's catch-all
|
|
58
|
+
* commit feed and build the decorating {@link IBlockChangeNotifier}. Returns the notifier plus the
|
|
59
|
+
* idempotent teardown for that catch-all subscription (the underlying `onAnyCollectionChange` unsub is
|
|
60
|
+
* itself idempotent), so the node assembly can release it on node stop alongside `host.stop()`.
|
|
61
|
+
*/
|
|
62
|
+
function buildCohortTopicChangeBridge(deps: CohortTopicChangeNotifierDeps): { notifier: IBlockChangeNotifier; unsubscribe: () => void } {
|
|
63
|
+
const unsubscribe = deps.source.onAnyCollectionChange((event) => originate(deps, event));
|
|
64
|
+
const notifier: IBlockChangeNotifier = {
|
|
65
|
+
onCollectionChange: (collectionId, listener): (() => void) => deps.source.onCollectionChange(collectionId, listener),
|
|
66
|
+
};
|
|
67
|
+
return { notifier, unsubscribe };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Run the membership gate, cert extraction, and origination hook for one change event, isolating throws. */
|
|
71
|
+
function originate(deps: CohortTopicChangeNotifierDeps, event: CollectionChangeEvent): void {
|
|
72
|
+
try {
|
|
73
|
+
if (!deps.selfIsCohortMember(event)) {
|
|
74
|
+
return; // not responsible for this topic's fan-out
|
|
75
|
+
}
|
|
76
|
+
const hook = deps.service.onLocalCommit;
|
|
77
|
+
if (!hook) {
|
|
78
|
+
return; // no reactivity/matchmaking consumer has attached an origination handler yet
|
|
79
|
+
}
|
|
80
|
+
const commitCert = deps.extractCommitCert(event);
|
|
81
|
+
if (!commitCert) {
|
|
82
|
+
return; // nothing authoritative to forward; never fabricate an unsigned cert (extractor logs why)
|
|
83
|
+
}
|
|
84
|
+
hook(event, commitCert);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
log('origination hook threw for collection=%s rev=%d: %o', event.collectionId, event.rev, err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Wire the cohort-topic origination bridge as `node`'s `blockChangeNotifier` (the value the
|
|
92
|
+
* `NetworkTransactor` consumes as its `localChangeNotifier`). Call this from the node assembly once a
|
|
93
|
+
* {@link CohortTopicService} is running on the node, passing the node's `StorageRepo` as `source` and
|
|
94
|
+
* the membership + cert-extraction seams.
|
|
95
|
+
*
|
|
96
|
+
* Returns the installed `notifier` plus an idempotent `unsubscribe` that tears down the catch-all
|
|
97
|
+
* origination subscription — the node assembly calls it on node stop (alongside `host.stop()`) so the
|
|
98
|
+
* origination feed is released before the node's transports close. Tearing it down stops further
|
|
99
|
+
* origination but leaves the per-collection `onCollectionChange` delegation intact (those subscriptions
|
|
100
|
+
* live on the `source` `StorageRepo`, not on the bridge).
|
|
101
|
+
*/
|
|
102
|
+
export function attachCohortChangeBridge(
|
|
103
|
+
node: { blockChangeNotifier?: IBlockChangeNotifier },
|
|
104
|
+
deps: CohortTopicChangeNotifierDeps,
|
|
105
|
+
): { notifier: IBlockChangeNotifier; unsubscribe: () => void } {
|
|
106
|
+
const bridge = buildCohortTopicChangeBridge(deps);
|
|
107
|
+
node.blockChangeNotifier = bridge.notifier;
|
|
108
|
+
return bridge;
|
|
109
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic gossip-cadence driver helpers (host-side, gap 5).
|
|
3
|
+
*
|
|
4
|
+
* The {@link import("./host.js").CohortTopicHost} owns a single repeating timer; on each tick it drives
|
|
5
|
+
* every live `CoordEngine`'s gossip round. The per-round work this module factors out — assembling a
|
|
6
|
+
* `CohortGossipV1` and accumulating/draining the registration-record deltas the renewal `touch`/`evicted`
|
|
7
|
+
* hooks emit — is split here so it is unit-testable without the timer or libp2p.
|
|
8
|
+
*
|
|
9
|
+
* db-p2p (not db-core) owns the cadence because the round interval, the peer-key signature on the gossip
|
|
10
|
+
* envelope, and the per-coord inbound routing all live at the FRET/libp2p boundary; db-core supplies the
|
|
11
|
+
* pure substrate (`selfWillingnessBits`, `toCohortTopicSummary`, `toGossipRecord`, the bus merge).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
bytesToB64url,
|
|
16
|
+
toGossipRecord,
|
|
17
|
+
willingnessBitsHex,
|
|
18
|
+
selfWillingnessBits,
|
|
19
|
+
type CohortGossipV1,
|
|
20
|
+
type CohortTopicSummary,
|
|
21
|
+
type GossipRecordRefV1,
|
|
22
|
+
type GossipRecordV1,
|
|
23
|
+
type LoadBarometerState,
|
|
24
|
+
type NodeProfile,
|
|
25
|
+
type RegistrationRecord,
|
|
26
|
+
} from "@optimystic/db-core";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Default gossip-round cadence in ms. There is no dedicated `gossip_round` constant in
|
|
30
|
+
* `docs/cohort-topic.md` §Configuration, so this is a derived, injectable default on the order of one
|
|
31
|
+
* round — a few seconds, aligned with the traffic window / ping cadence (`ping_interval = ttl/3` is
|
|
32
|
+
* 30 s Core, so a sub-round of that). The membership refresh (`T_membership_refresh`, 5 min) and the
|
|
33
|
+
* demotion hysteresis (`T_demote`, 5 min) are gated by elapsed-time **inside** their modules, so the
|
|
34
|
+
* driver can tick fast and let those modules decide when to act.
|
|
35
|
+
*/
|
|
36
|
+
export const DEFAULT_GOSSIP_INTERVAL_MS = 5_000;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Per-`CoordEngine` queue of registration-record deltas accumulated between gossip rounds. The renewal
|
|
40
|
+
* cohort side calls {@link PendingDeltas.touch} on every served ping/re-attach and
|
|
41
|
+
* {@link PendingDeltas.evicted} on every TTL sweep; the next round {@link PendingDeltas.drain}s the
|
|
42
|
+
* batch into the gossip frame and clears it — one broadcast per round rather than one per ping.
|
|
43
|
+
*/
|
|
44
|
+
export interface PendingDeltas {
|
|
45
|
+
/** Upsert a fresh/touched record (keyed by `(topicId, participantId)`; last write wins on `lastPing`). */
|
|
46
|
+
touch(rec: RegistrationRecord): void;
|
|
47
|
+
/** Queue an eviction ref and drop any pending record for the same key (a stale record can't also re-advertise). */
|
|
48
|
+
evicted(rec: RegistrationRecord): void;
|
|
49
|
+
/** True iff nothing is queued. */
|
|
50
|
+
isEmpty(): boolean;
|
|
51
|
+
/** Drain the queue into wire-shaped deltas, clearing it. */
|
|
52
|
+
drain(): { records: GossipRecordV1[]; evicted: GossipRecordRefV1[] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Build an empty {@link PendingDeltas} queue. */
|
|
56
|
+
export function createPendingDeltas(): PendingDeltas {
|
|
57
|
+
const records = new Map<string, RegistrationRecord>();
|
|
58
|
+
const evicted = new Map<string, GossipRecordRefV1>();
|
|
59
|
+
const keyOf = (topicId: Uint8Array, participantId: Uint8Array): string =>
|
|
60
|
+
`${bytesToB64url(topicId)}|${bytesToB64url(participantId)}`;
|
|
61
|
+
return {
|
|
62
|
+
touch(rec: RegistrationRecord): void {
|
|
63
|
+
const key = keyOf(rec.topicId, rec.participantId);
|
|
64
|
+
const held = records.get(key);
|
|
65
|
+
// Last-writer-wins on lastPing: never let an older touch shadow a newer one queued the same round.
|
|
66
|
+
if (held === undefined || rec.lastPing >= held.lastPing) {
|
|
67
|
+
records.set(key, rec);
|
|
68
|
+
}
|
|
69
|
+
evicted.delete(key); // a live touch supersedes a pending eviction (eviction-vs-late-touch)
|
|
70
|
+
},
|
|
71
|
+
evicted(rec: RegistrationRecord): void {
|
|
72
|
+
const key = keyOf(rec.topicId, rec.participantId);
|
|
73
|
+
evicted.set(key, { topicId: bytesToB64url(rec.topicId), participantId: bytesToB64url(rec.participantId) });
|
|
74
|
+
records.delete(key);
|
|
75
|
+
},
|
|
76
|
+
isEmpty(): boolean {
|
|
77
|
+
return records.size === 0 && evicted.size === 0;
|
|
78
|
+
},
|
|
79
|
+
drain(): { records: GossipRecordV1[]; evicted: GossipRecordRefV1[] } {
|
|
80
|
+
const out = {
|
|
81
|
+
records: [...records.values()].map(toGossipRecord),
|
|
82
|
+
evicted: [...evicted.values()],
|
|
83
|
+
};
|
|
84
|
+
records.clear();
|
|
85
|
+
evicted.clear();
|
|
86
|
+
return out;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Inputs to {@link buildCohortGossip} — the per-round signals already gathered by the engine. */
|
|
92
|
+
export interface GossipFrameInputs {
|
|
93
|
+
/** This member's dialable id, base64url (the `fromMember` / view key). */
|
|
94
|
+
readonly fromMember: string;
|
|
95
|
+
/** This cohort's served coord, base64url (the inbound routing key). */
|
|
96
|
+
readonly coord: string;
|
|
97
|
+
/** This cohort's epoch, base64url. */
|
|
98
|
+
readonly cohortEpoch: string;
|
|
99
|
+
/** This node's tier profile (the `∧` half of the willingness vector). */
|
|
100
|
+
readonly profile: NodeProfile;
|
|
101
|
+
/** This member's load barometer (load buckets + the load-shed half of willingness). */
|
|
102
|
+
readonly barometer: LoadBarometerState;
|
|
103
|
+
/** Cohort-wide observation window (seconds) for the rate fields in `topicSummaries`. */
|
|
104
|
+
readonly windowSeconds: number;
|
|
105
|
+
/** Per-resident-topic summaries (own published counts), already frozen for this round. */
|
|
106
|
+
readonly topicSummaries: CohortTopicSummary[];
|
|
107
|
+
/** Drained fresh/touched record deltas for this round. */
|
|
108
|
+
readonly records: GossipRecordV1[];
|
|
109
|
+
/** Drained eviction refs for this round. */
|
|
110
|
+
readonly evicted: GossipRecordRefV1[];
|
|
111
|
+
/** Round timestamp, unix ms. */
|
|
112
|
+
readonly timestamp: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Assemble one round's {@link CohortGossipV1}, or `undefined` when this engine is **idle** — no resident
|
|
117
|
+
* topics and no pending deltas, so it has nothing for siblings and the host skips the broadcast (idle
|
|
118
|
+
* empty engines cost no gossip). `willingnessBits` is `profile ∧ load` ({@link selfWillingnessBits}); the
|
|
119
|
+
* `signature` slot is left empty for the host's peer-key signer to fill before broadcast.
|
|
120
|
+
*/
|
|
121
|
+
export function buildCohortGossip(i: GossipFrameInputs): CohortGossipV1 | undefined {
|
|
122
|
+
if (i.topicSummaries.length === 0 && i.records.length === 0 && i.evicted.length === 0) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
const g: CohortGossipV1 = {
|
|
126
|
+
v: 1,
|
|
127
|
+
fromMember: i.fromMember,
|
|
128
|
+
coord: i.coord,
|
|
129
|
+
cohortEpoch: i.cohortEpoch,
|
|
130
|
+
willingnessBits: willingnessBitsHex(selfWillingnessBits(i.profile, i.barometer)),
|
|
131
|
+
loadBuckets: i.barometer.loadBuckets(),
|
|
132
|
+
windowSeconds: i.windowSeconds,
|
|
133
|
+
topicSummaries: i.topicSummaries,
|
|
134
|
+
timestamp: i.timestamp,
|
|
135
|
+
signature: "",
|
|
136
|
+
};
|
|
137
|
+
if (i.records.length > 0) {
|
|
138
|
+
g.records = i.records;
|
|
139
|
+
}
|
|
140
|
+
if (i.evicted.length > 0) {
|
|
141
|
+
g.evicted = i.evicted;
|
|
142
|
+
}
|
|
143
|
+
return g;
|
|
144
|
+
}
|