@optimystic/db-p2p 0.22.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cluster/client.d.ts +10 -0
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +30 -1
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +39 -14
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +226 -119
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/service.d.ts +32 -1
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +43 -2
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cohort-topic/host.js +34 -11
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +37 -7
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +77 -19
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +9 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +88 -2
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +134 -28
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +25 -1
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/logger.d.ts +17 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +19 -2
- package/dist/src/logger.js.map +1 -1
- package/dist/src/matchmaking/query-transport.js +3 -3
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/owned-block-seed.d.ts +6 -3
- package/dist/src/owned-block-seed.d.ts.map +1 -1
- package/dist/src/owned-block-seed.js +16 -3
- package/dist/src/owned-block-seed.js.map +1 -1
- package/dist/src/peer-address-book.d.ts +72 -0
- package/dist/src/peer-address-book.d.ts.map +1 -0
- package/dist/src/peer-address-book.js +123 -0
- package/dist/src/peer-address-book.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts +4 -4
- package/dist/src/reactivity/notify-transport.js +6 -6
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -2
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/reactivity/recover-transport.d.ts +6 -2
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +7 -3
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +11 -2
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +30 -0
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +95 -3
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +62 -9
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +242 -73
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/rn.d.ts +3 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +3 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +83 -0
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
- package/dist/src/storage/cached-raw-storage.js +152 -0
- package/dist/src/storage/cached-raw-storage.js.map +1 -0
- package/dist/src/storage/cached-store-driver.d.ts +186 -0
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
- package/dist/src/storage/cached-store-driver.js +775 -0
- package/dist/src/storage/cached-store-driver.js.map +1 -0
- package/dist/src/storage/i-raw-storage.d.ts +12 -5
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +234 -0
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
- package/dist/src/storage/shared-cache-pool.js +354 -0
- package/dist/src/storage/shared-cache-pool.js.map +1 -0
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +35 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +668 -668
- package/src/cluster/block-transfer.ts +424 -424
- package/src/cluster/client.ts +119 -88
- package/src/cluster/cluster-error.ts +64 -64
- package/src/cluster/cluster-policy.ts +203 -203
- package/src/cluster/cluster-repo.ts +242 -122
- package/src/cluster/cluster-size-coupling.ts +45 -45
- package/src/cluster/commit-cert.ts +139 -139
- package/src/cluster/i-transaction-state-store.ts +43 -43
- package/src/cluster/memory-transaction-state-store.ts +56 -56
- package/src/cluster/peer-key-binding.ts +37 -37
- package/src/cluster/persistent-transaction-state-store.ts +92 -92
- package/src/cluster/quorum-restore.ts +223 -223
- package/src/cluster/reconcile-block.ts +203 -203
- package/src/cluster/service.ts +293 -241
- package/src/cluster/supermajority-coupling.ts +37 -37
- package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
- package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
- package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
- package/src/cohort-topic/change-bridge.ts +109 -109
- package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
- package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
- package/src/cohort-topic/fret-trust-anchor.ts +153 -153
- package/src/cohort-topic/host.ts +42 -11
- package/src/cohort-topic/index.ts +13 -13
- package/src/cohort-topic/membership-publish-sink.ts +20 -20
- package/src/cohort-topic/membership-source.ts +68 -68
- package/src/cohort-topic/peer-codec.ts +31 -31
- package/src/cohort-topic/peer-sig.ts +86 -86
- package/src/cohort-topic/protocols.ts +71 -71
- package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
- package/src/cohort-topic/size-estimator.ts +16 -16
- package/src/cohort-topic/stream-util.ts +79 -19
- package/src/cohort-topic/threshold-crypto.ts +239 -239
- package/src/cohort-topic/topic-router.ts +77 -77
- package/src/dispute/arbitrator-selection.ts +138 -138
- package/src/dispute/cascade.ts +524 -524
- package/src/dispute/dispute-service.ts +11 -5
- package/src/dispute/invalidation.ts +625 -625
- package/src/inbound-authorization.ts +190 -190
- package/src/index.ts +52 -49
- package/src/libp2p-key-network.ts +1120 -990
- package/src/libp2p-node-base.ts +1675 -1651
- package/src/libp2p-node-rn.ts +30 -30
- package/src/libp2p-node.ts +36 -36
- package/src/logger.ts +19 -2
- package/src/matchmaking/aggregate-counts.ts +104 -104
- package/src/matchmaking/index.ts +20 -20
- package/src/matchmaking/module.ts +363 -363
- package/src/matchmaking/protocols.ts +51 -51
- package/src/matchmaking/provider-manager.ts +95 -95
- package/src/matchmaking/query-handler.ts +88 -88
- package/src/matchmaking/query-transport.ts +3 -3
- package/src/matchmaking/seeker-manager.ts +64 -64
- package/src/matchmaking/seeker-walk-client.ts +293 -293
- package/src/matchmaking/traffic-validation.ts +195 -195
- package/src/optimystic-node.ts +36 -36
- package/src/owned-block-seed.ts +53 -40
- package/src/peer-address-book.ts +149 -0
- package/src/protocol-limits.ts +33 -33
- package/src/reactivity/forwarder-host.ts +438 -438
- package/src/reactivity/index.ts +19 -19
- package/src/reactivity/notify-transport.ts +144 -144
- package/src/reactivity/origination-manager.ts +192 -192
- package/src/reactivity/protocols.ts +61 -61
- package/src/reactivity/push-state-gossip.ts +291 -291
- package/src/reactivity/recover-transport.ts +7 -3
- package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
- package/src/reactivity/subscriber-registry.ts +96 -96
- package/src/reactivity/subscription-manager.ts +450 -450
- package/src/reactivity/topic-bytes.ts +37 -37
- package/src/repo/client.ts +12 -2
- package/src/repo/cluster-coordinator.ts +99 -3
- package/src/repo/coordinator-repo.ts +281 -74
- package/src/repo/types.ts +7 -7
- package/src/rn.ts +39 -36
- package/src/rpc-deadline.ts +45 -45
- package/src/storage/arachnode-partition.ts +74 -74
- package/src/storage/cached-raw-storage.ts +180 -0
- package/src/storage/cached-store-driver.ts +859 -0
- package/src/storage/i-kv-store.ts +8 -8
- package/src/storage/i-raw-storage.ts +12 -5
- package/src/storage/kv-raw-storage.ts +135 -135
- package/src/storage/memory-kv-store.ts +28 -28
- package/src/storage/memory-storage.ts +25 -25
- package/src/storage/memory-store-driver.ts +157 -157
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +80 -80
- package/src/storage/ring-selector.ts +317 -317
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/shared-cache-pool.ts +452 -0
- package/src/storage/storage-repo.ts +1014 -1014
- package/src/testing/cohort-topic-mesh-harness.ts +673 -663
- package/src/testing/index.ts +8 -8
- package/src/testing/matchmaking-mesh-harness.ts +475 -475
- package/src/testing/raw-storage-conformance.ts +453 -417
- package/src/testing/reactivity-mesh-harness.ts +922 -922
- package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
- package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
- package/dist/src/storage/restoration-coordinator-v2.js +0 -172
- package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reactivity — the one pinned `BlockId` → raw tail-bytes encoding, shared by origination and subscription.
|
|
3
|
-
*
|
|
4
|
-
* `reactivityTopicId` (db-core) hashes `H(tailBytes ‖ "reactivity")` to derive the topic's `coord_0`
|
|
5
|
-
* cohort. Origination (the cohort-change membership gate, {@link
|
|
6
|
-
* import("../cohort-topic/reactivity-membership-gate.js").createReactivitySelfMembershipGate}) and the
|
|
7
|
-
* subscriber side (the production subscribe factory that converts a `BlockId` `tailIdAtAttach` to the bytes
|
|
8
|
-
* {@link import("./subscription-manager.js").ReactivitySubscriptionManager} feeds `reactivityTopicId`) MUST
|
|
9
|
-
* feed it the **same** bytes for a given tail, or they resolve different coords and origination silently
|
|
10
|
-
* never reaches subscribers (green tests, dead feature).
|
|
11
|
-
*
|
|
12
|
-
* This module is that single source of truth: `reactivityTailBytes(tailId) = TextEncoder().encode(tailId)`,
|
|
13
|
-
* the synchronous block-id→bytes convention the rest of db-p2p already uses for ring hashing of tail ids
|
|
14
|
-
* (`cluster/client.ts`, `repo/client.ts`). db-core's async `blockIdToBytes` (which `sha256`s the utf8 bytes
|
|
15
|
-
* first) is deliberately NOT used: it would (a) double-hash relative to the spec's `H(tailId ‖ "reactivity")`
|
|
16
|
-
* and (b) force the synchronous origination gate to become async.
|
|
17
|
-
*
|
|
18
|
-
* It lives in the reactivity surface (not buried in the gate) so the subscribe path and the gate import the
|
|
19
|
-
* exact same function rather than re-deriving the encoding independently. The
|
|
20
|
-
* `cohort-topic/reactivity-membership-gate.ts` module re-exports it for its existing callers.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import type { BlockId } from "@optimystic/db-core";
|
|
24
|
-
|
|
25
|
-
const utf8 = new TextEncoder();
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The pinned `BlockId` → raw tail bytes encoding fed into `reactivityTopicId`: the **raw** utf8 bytes of the
|
|
29
|
-
* tail block-id string (NOT a pre-hashed routing key, NOT db-core's double-hashing `blockIdToBytes`).
|
|
30
|
-
*
|
|
31
|
-
* Load-bearing: origination and the subscriber side must call this **same** function for a given tail (see
|
|
32
|
-
* the module doc) — a mismatch resolves different `coord_0`s and origination silently never reaches
|
|
33
|
-
* subscribers. The `topic-bytes-encoding` spec pins the coord-equality both ways.
|
|
34
|
-
*/
|
|
35
|
-
export function reactivityTailBytes(tailId: BlockId): Uint8Array {
|
|
36
|
-
return utf8.encode(tailId);
|
|
37
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — the one pinned `BlockId` → raw tail-bytes encoding, shared by origination and subscription.
|
|
3
|
+
*
|
|
4
|
+
* `reactivityTopicId` (db-core) hashes `H(tailBytes ‖ "reactivity")` to derive the topic's `coord_0`
|
|
5
|
+
* cohort. Origination (the cohort-change membership gate, {@link
|
|
6
|
+
* import("../cohort-topic/reactivity-membership-gate.js").createReactivitySelfMembershipGate}) and the
|
|
7
|
+
* subscriber side (the production subscribe factory that converts a `BlockId` `tailIdAtAttach` to the bytes
|
|
8
|
+
* {@link import("./subscription-manager.js").ReactivitySubscriptionManager} feeds `reactivityTopicId`) MUST
|
|
9
|
+
* feed it the **same** bytes for a given tail, or they resolve different coords and origination silently
|
|
10
|
+
* never reaches subscribers (green tests, dead feature).
|
|
11
|
+
*
|
|
12
|
+
* This module is that single source of truth: `reactivityTailBytes(tailId) = TextEncoder().encode(tailId)`,
|
|
13
|
+
* the synchronous block-id→bytes convention the rest of db-p2p already uses for ring hashing of tail ids
|
|
14
|
+
* (`cluster/client.ts`, `repo/client.ts`). db-core's async `blockIdToBytes` (which `sha256`s the utf8 bytes
|
|
15
|
+
* first) is deliberately NOT used: it would (a) double-hash relative to the spec's `H(tailId ‖ "reactivity")`
|
|
16
|
+
* and (b) force the synchronous origination gate to become async.
|
|
17
|
+
*
|
|
18
|
+
* It lives in the reactivity surface (not buried in the gate) so the subscribe path and the gate import the
|
|
19
|
+
* exact same function rather than re-deriving the encoding independently. The
|
|
20
|
+
* `cohort-topic/reactivity-membership-gate.ts` module re-exports it for its existing callers.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { BlockId } from "@optimystic/db-core";
|
|
24
|
+
|
|
25
|
+
const utf8 = new TextEncoder();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The pinned `BlockId` → raw tail bytes encoding fed into `reactivityTopicId`: the **raw** utf8 bytes of the
|
|
29
|
+
* tail block-id string (NOT a pre-hashed routing key, NOT db-core's double-hashing `blockIdToBytes`).
|
|
30
|
+
*
|
|
31
|
+
* Load-bearing: origination and the subscriber side must call this **same** function for a given tail (see
|
|
32
|
+
* the module doc) — a mismatch resolves different `coord_0`s and origination silently never reaches
|
|
33
|
+
* subscribers. The `topic-bytes-encoding` spec pins the coord-equality both ways.
|
|
34
|
+
*/
|
|
35
|
+
export function reactivityTailBytes(tailId: BlockId): Uint8Array {
|
|
36
|
+
return utf8.encode(tailId);
|
|
37
|
+
}
|
package/src/repo/client.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
import type { RepoMessage } from "@optimystic/db-core";
|
|
6
6
|
import { blockIdsForTransforms, blockIdToBytes } from "@optimystic/db-core";
|
|
7
7
|
import { ProtocolClient } from "../protocol-client.js";
|
|
8
|
+
import type { RedirectPayload } from "./redirect.js";
|
|
8
9
|
import { MAX_BLOCK_MESSAGE_BYTES } from "../protocol-limits.js";
|
|
9
10
|
import { peerIdFromString } from "@libp2p/peer-id";
|
|
10
11
|
|
|
@@ -102,16 +103,25 @@ export class RepoClient extends ProtocolClient implements IRepo {
|
|
|
102
103
|
clearTimeout(timer)
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
// Type the redirect branch against the payload the service actually produces
|
|
107
|
+
// (`RedirectPayload`) rather than reading it off the `any` response — the untyped read
|
|
108
|
+
// is what let the `addrs` the sender embedded fall on the floor.
|
|
109
|
+
const redirectResponse = response as Partial<RedirectPayload>
|
|
110
|
+
if (redirectResponse?.redirect?.peers?.length) {
|
|
106
111
|
if (hop >= 2) {
|
|
107
112
|
throw new Error('Redirect loop detected in RepoClient (max hops reached)')
|
|
108
113
|
}
|
|
109
114
|
const currentIdStr = this.peerId.toString()
|
|
110
|
-
const
|
|
115
|
+
const peers = redirectResponse.redirect.peers
|
|
116
|
+
const next = peers.find((p) => p.id !== currentIdStr) ?? peers[0]!
|
|
111
117
|
const nextId = peerIdFromString(next.id)
|
|
112
118
|
if (next.id === currentIdStr) {
|
|
113
119
|
throw new Error('Redirect loop detected in RepoClient (same peer)')
|
|
114
120
|
}
|
|
121
|
+
// Learn the redirect target's addresses BEFORE dialing it: the redirect is the only
|
|
122
|
+
// notice we get that this peer matters, and if it is relay-only we have no address
|
|
123
|
+
// for it at all. Merging after the dial would help only some later hop.
|
|
124
|
+
this.peerNetwork.recordPeerAddresses?.(nextId, next.addrs ?? [])
|
|
115
125
|
// cache hint — await so the recorded key bytes match what findCoordinator
|
|
116
126
|
// later looks up (blockIdToBytes is async); deterministic ordering also
|
|
117
127
|
// keeps the hint testable. The redirect retry below is async anyway.
|
|
@@ -32,6 +32,27 @@ export class ValidatorRejectionError extends Error {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The transaction lost a conflict race: one or more members answered with a signed `conflict`
|
|
37
|
+
* vote (they hold a rival transaction that won the deterministic race on the same blocks) and
|
|
38
|
+
* approvals fell short of super-majority. Distinct from {@link ValidatorRejectionError} — nobody
|
|
39
|
+
* judged this write invalid; it lost an optimistic-concurrency race and a fresh retry can win.
|
|
40
|
+
* `CoordinatorRepo.pend` converts this into a `StaleFailure` with `conflict: true` so the normal
|
|
41
|
+
* retry machinery (`isConflictFailure`) absorbs it; it should escape as a thrown error only from
|
|
42
|
+
* paths other than pend. The conflicting peers and the winning hashes ride as structured data
|
|
43
|
+
* (from the signed `conflictWith` fields), never parsed out of prose.
|
|
44
|
+
*/
|
|
45
|
+
export class ConflictRaceLostError extends Error {
|
|
46
|
+
constructor(
|
|
47
|
+
message: string,
|
|
48
|
+
/** peerId → messageHash of the rival transaction that member holds as the race winner. */
|
|
49
|
+
readonly conflicts: Record<string, string>
|
|
50
|
+
) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = 'ConflictRaceLostError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
/** Cancel handle for an injected timer; cancels a not-yet-fired timer (safe no-op after fire/cancel). */
|
|
36
57
|
export type TimerCancel = () => void;
|
|
37
58
|
|
|
@@ -333,18 +354,21 @@ export class ClusterCoordinator {
|
|
|
333
354
|
const promised = await this.collectPromises(peers, record);
|
|
334
355
|
const superMajority = Math.ceil(peerCount * this.cfg.superMajorityThreshold);
|
|
335
356
|
|
|
336
|
-
// Count approvals and
|
|
357
|
+
// Count approvals, rejections and conflict votes separately. A `conflict` vote is a member
|
|
358
|
+
// saying "not now — I hold the race winner": it must count toward NEITHER approvals NOR
|
|
359
|
+
// rejections, or a lost race would masquerade as a validator rejection (permanent) or as
|
|
360
|
+
// silence (indistinguishable from an unreachable cohort) — both wrong.
|
|
337
361
|
const promises = promised.record.promises;
|
|
338
362
|
const approvalCount = Object.values(promises).filter(sig => sig.type === 'approve').length;
|
|
339
363
|
const rejectionCount = Object.values(promises).filter(sig => sig.type === 'reject').length;
|
|
364
|
+
const conflictCount = Object.values(promises).filter(sig => sig.type === 'conflict').length;
|
|
340
365
|
|
|
341
366
|
// Check if rejections make super-majority impossible
|
|
342
367
|
// If more than (peerCount - superMajority) nodes reject, we can never reach super-majority
|
|
343
368
|
const maxAllowedRejections = peerCount - superMajority;
|
|
344
369
|
if (rejectionCount > maxAllowedRejections) {
|
|
345
370
|
const rejectReasonsByPeer = Object.fromEntries(Object.entries(promises)
|
|
346
|
-
.
|
|
347
|
-
.map(([peerId, sig]) => [peerId, sig.rejectReason ?? 'unknown']));
|
|
371
|
+
.flatMap(([peerId, sig]) => sig.type === 'reject' ? [[peerId, sig.rejectReason ?? 'unknown'] as const] : []));
|
|
348
372
|
const rejectReasons = Object.entries(rejectReasonsByPeer)
|
|
349
373
|
.map(([peerId, reason]) => `${peerId}: ${reason}`)
|
|
350
374
|
.join('; ');
|
|
@@ -356,11 +380,46 @@ export class ClusterCoordinator {
|
|
|
356
380
|
reasons: rejectReasons
|
|
357
381
|
});
|
|
358
382
|
this.updateTransactionRecord(promised.record, 'rejected-by-validators');
|
|
383
|
+
// Abandoning here without telling anyone leaves every member that voted holding this
|
|
384
|
+
// transaction in its own reservation table, blocking its blocks until that member's
|
|
385
|
+
// staleness sweep fires — and each retry we throw back to the caller plants a fresh
|
|
386
|
+
// reservation, so the block never frees. The merged record carries enough signed
|
|
387
|
+
// rejections to *prove* the transaction is dead, so replaying it to the cohort makes
|
|
388
|
+
// every member recompute `Rejected` and clear immediately. Proof-carrying, so a member
|
|
389
|
+
// need not trust us: it verifies the signatures it is shown.
|
|
390
|
+
this.broadcastAbandonment(promised.record, 'rejected-by-validators');
|
|
359
391
|
throw new ValidatorRejectionError(
|
|
360
392
|
`Transaction rejected by validators (${rejectionCount}/${peerCount} rejected): ${rejectReasons}`,
|
|
361
393
|
rejectReasonsByPeer);
|
|
362
394
|
}
|
|
363
395
|
|
|
396
|
+
// A conflict-answered shortfall is a LOST RACE, not a validator verdict and not silence.
|
|
397
|
+
// Checked after the rejection threshold (a genuine validator rejection still wins) and
|
|
398
|
+
// before the generic shortfall (which must stay reserved for the genuinely-silent cohort).
|
|
399
|
+
if (conflictCount > 0 && approvalCount < superMajority) {
|
|
400
|
+
const conflicts = Object.fromEntries(Object.entries(promises)
|
|
401
|
+
.flatMap(([peerId, sig]) => sig.type === 'conflict' ? [[peerId, sig.conflictWith] as const] : []));
|
|
402
|
+
log('cluster-tx:conflict-race-lost', {
|
|
403
|
+
messageHash: record.messageHash,
|
|
404
|
+
peerCount,
|
|
405
|
+
approvals: approvalCount,
|
|
406
|
+
rejections: rejectionCount,
|
|
407
|
+
conflicts,
|
|
408
|
+
superMajority
|
|
409
|
+
});
|
|
410
|
+
this.updateTransactionRecord(promised.record, 'conflict-race-lost');
|
|
411
|
+
// Broadcast only when the merged record itself PROVES the transaction can no longer reach
|
|
412
|
+
// super-majority (members re-derive ConflictSuperseded/Rejected from the signed votes and
|
|
413
|
+
// clear their reservations immediately). Below that bar the record proves nothing and a
|
|
414
|
+
// broadcast would be the unauthenticated "forget this" the shortfall NOTE below refuses.
|
|
415
|
+
if (rejectionCount + conflictCount > maxAllowedRejections) {
|
|
416
|
+
this.broadcastAbandonment(promised.record, 'conflict-race-lost');
|
|
417
|
+
}
|
|
418
|
+
throw new ConflictRaceLostError(
|
|
419
|
+
`Conflict race lost: ${conflictCount}/${peerCount} member(s) hold a conflicting winner (${approvalCount}/${superMajority} approvals)`,
|
|
420
|
+
conflicts);
|
|
421
|
+
}
|
|
422
|
+
|
|
364
423
|
if (peerCount > 1 && approvalCount < superMajority) {
|
|
365
424
|
log('cluster-tx:supermajority-failed', {
|
|
366
425
|
messageHash: record.messageHash,
|
|
@@ -371,6 +430,15 @@ export class ClusterCoordinator {
|
|
|
371
430
|
threshold: this.cfg.superMajorityThreshold
|
|
372
431
|
});
|
|
373
432
|
this.updateTransactionRecord(promised.record, 'supermajority-failed');
|
|
433
|
+
// NOTE: deliberately NOT broadcast, unlike the rejected-by-validators branch above. With
|
|
434
|
+
// conflict-answered shortfalls peeled off above, we get here only because peers did not
|
|
435
|
+
// answer at all, so the record carries no signed evidence that the transaction is dead — a
|
|
436
|
+
// broadcast would be an unauthenticated "forget this" that any caller could use to clear a
|
|
437
|
+
// live transaction out of a member's reservation table. Members that DID vote are freed by
|
|
438
|
+
// their own staleness sweep instead.
|
|
439
|
+
// NOTE: the message below is load-bearing wire text — the consuming repo
|
|
440
|
+
// (sereus cadre-core control-write-retry) matches it verbatim to retry a genuinely-silent
|
|
441
|
+
// cohort. Keep it byte-identical, and never fold conflict votes into its rejection count.
|
|
374
442
|
throw new Error(`Failed to get super-majority: ${approvalCount}/${peerCount} approvals (needed ${superMajority}, ${rejectionCount} rejections)`);
|
|
375
443
|
}
|
|
376
444
|
|
|
@@ -696,6 +764,34 @@ export class ClusterCoordinator {
|
|
|
696
764
|
return { failures };
|
|
697
765
|
}
|
|
698
766
|
|
|
767
|
+
/**
|
|
768
|
+
* Fire-and-forget replay of an abandoned transaction's record to every peer in its cohort.
|
|
769
|
+
*
|
|
770
|
+
* Called only where the record itself proves the transaction is dead (enough signed rejections that
|
|
771
|
+
* super-majority is unreachable). Each member re-derives `TransactionPhase.Rejected` from the votes
|
|
772
|
+
* it verifies and drops the entry from its own reservation table, freeing the blocks immediately
|
|
773
|
+
* instead of after its 2 s staleness window. No new message type and no wire-format change — this is
|
|
774
|
+
* the same `update()` every other phase uses.
|
|
775
|
+
*
|
|
776
|
+
* Never awaited into the caller's throw and never rethrows: an abandonment must not turn into a
|
|
777
|
+
* *different* failure, and the staleness sweep remains the backstop if delivery fails.
|
|
778
|
+
*/
|
|
779
|
+
private broadcastAbandonment(record: ClusterRecord, reason: string): void {
|
|
780
|
+
const peerIds = Object.keys(record.peers);
|
|
781
|
+
log('cluster-tx:abandon-broadcast', { messageHash: record.messageHash, reason, peerIds });
|
|
782
|
+
void Promise.all(peerIds.map(async peerIdStr => {
|
|
783
|
+
try {
|
|
784
|
+
await this.updateMember(peerIdStr, record, 0, 'abandon-broadcast');
|
|
785
|
+
} catch (err) {
|
|
786
|
+
log('cluster-tx:abandon-broadcast-error', {
|
|
787
|
+
messageHash: record.messageHash,
|
|
788
|
+
peerId: peerIdStr,
|
|
789
|
+
error: err instanceof Error ? err.message : String(err)
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}));
|
|
793
|
+
}
|
|
794
|
+
|
|
699
795
|
private updateTransactionRecord(record: ClusterRecord, stage: string): void {
|
|
700
796
|
const state = this.transactions.get(record.messageHash);
|
|
701
797
|
if (!state) {
|