@optimystic/db-p2p 0.17.0 → 0.19.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/dist/src/cluster/block-transfer-service.d.ts +10 -0
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +2 -1
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +112 -0
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -0
- package/dist/src/cluster/cluster-policy.js +88 -0
- package/dist/src/cluster/cluster-policy.js.map +1 -0
- package/dist/src/cluster/cluster-repo.d.ts +35 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +95 -19
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/quorum-restore.d.ts +25 -3
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +27 -3
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts +10 -2
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +38 -18
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +8 -0
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/inbound-authorization.d.ts +6 -0
- package/dist/src/inbound-authorization.d.ts.map +1 -1
- package/dist/src/inbound-authorization.js +6 -0
- package/dist/src/inbound-authorization.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +66 -4
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +130 -17
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +22 -23
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +45 -34
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +21 -3
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +27 -5
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +88 -28
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +287 -81
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +9 -0
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +77 -7
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +17 -0
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +27 -4
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +20 -0
- package/src/cluster/block-transfer-service.ts +9 -1
- package/src/cluster/cluster-policy.ts +152 -0
- package/src/cluster/cluster-repo.ts +100 -22
- package/src/cluster/quorum-restore.ts +28 -3
- package/src/cluster/reconcile-block.ts +52 -19
- package/src/cluster/spread-on-churn.ts +8 -0
- package/src/inbound-authorization.ts +6 -0
- package/src/libp2p-key-network.ts +958 -807
- package/src/libp2p-node-base.ts +65 -57
- package/src/repo/cluster-coordinator.ts +30 -6
- package/src/repo/coordinator-repo.ts +329 -91
- package/src/storage/storage-repo.ts +81 -10
- package/src/testing/mesh-harness.ts +34 -4
package/src/libp2p-node-base.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { IRawStorage } from './storage/i-raw-storage.js';
|
|
|
21
21
|
import { seedOwnedBlocksFromStorage } from './owned-block-seed.js';
|
|
22
22
|
import { clusterMember, type ReconcileBlockCallback, type CommitCertificateSink, type DeriveExpectedClusterCallback } from './cluster/cluster-repo.js';
|
|
23
23
|
import { createReconcileBlock } from './cluster/reconcile-block.js';
|
|
24
|
+
import { resolveClusterPolicy, type ClusterPolicyOptions } from './cluster/cluster-policy.js';
|
|
24
25
|
import { createCommitCertStore, makeClusterCommitCertExtractor, type CommitCertStore } from './cluster/commit-cert.js';
|
|
25
26
|
import { coordinatorRepo } from './repo/coordinator-repo.js';
|
|
26
27
|
import { Libp2pKeyPeerNetwork, type NetworkMode, type NetworkStatePersistence } from './libp2p-key-network.js';
|
|
@@ -70,7 +71,6 @@ import {
|
|
|
70
71
|
reactivityNodePolicy,
|
|
71
72
|
createTierAddressing,
|
|
72
73
|
createRingHash,
|
|
73
|
-
DEFAULT_SUPER_MAJORITY_THRESHOLD,
|
|
74
74
|
Tier,
|
|
75
75
|
b64urlToBytes,
|
|
76
76
|
bytesToB64url,
|
|
@@ -135,7 +135,12 @@ const wiringLog = createLogger('node-wiring');
|
|
|
135
135
|
/** Factory function or instance for creating raw storage */
|
|
136
136
|
export type RawStorageProvider = IRawStorage | (() => IRawStorage);
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
/**
|
|
139
|
+
* `ClusterPolicyOptions` is intersected in, not restated: `resolveClusterPolicy` consumes those
|
|
140
|
+
* fields structurally, so a second copy of the shape here would let a newly added knob compile and
|
|
141
|
+
* be silently ignored. See `cluster/cluster-policy.ts` for what each one resolves to.
|
|
142
|
+
*/
|
|
143
|
+
export type NodeOptions = ClusterPolicyOptions & {
|
|
139
144
|
/**
|
|
140
145
|
* Network port. Only used by the default `listenAddrs` fallback.
|
|
141
146
|
* For non-TCP transports (e.g. WebSockets), set `listenAddrs` explicitly.
|
|
@@ -174,29 +179,20 @@ export type NodeOptions = {
|
|
|
174
179
|
relayServerInit?: CircuitRelayServerInit;
|
|
175
180
|
/** Storage provider - either an IRawStorage instance or a factory function. Defaults to MemoryRawStorage if not provided. */
|
|
176
181
|
storage?: RawStorageProvider;
|
|
177
|
-
/**
|
|
178
|
-
* Desired cluster size per key (default 10). Beyond sizing the cohort, this is the
|
|
179
|
-
* node's declaration of how many peers *should* exist to corroborate a claim: the
|
|
180
|
-
* read-repair corroboration floor is measured against it, so a genuine two-node
|
|
181
|
-
* deployment must set `clusterSize: 2` for its members to be able to repair each
|
|
182
|
-
* other (see `CoordinatorRepo.corroboratorCapacity`).
|
|
183
|
-
*/
|
|
184
|
-
clusterSize?: number;
|
|
185
|
-
clusterPolicy?: {
|
|
186
|
-
allowDownsize?: boolean;
|
|
187
|
-
sizeTolerance?: number; // acceptable relative difference (e.g. 0.5 = +/-50%)
|
|
188
|
-
superMajorityThreshold?: number; // fraction of peers needed for super-majority (default: DEFAULT_SUPER_MAJORITY_THRESHOLD = 0.75)
|
|
189
|
-
/**
|
|
190
|
-
* Opt in to transacting below the safe cluster-size floor when FRET has no confident
|
|
191
|
-
* network-size estimate — the membership-admission and coordinator small-cluster gates
|
|
192
|
-
* both fail closed without it. Default false. Turn on only for single-node / local dev
|
|
193
|
-
* meshes that knowingly run undersized.
|
|
194
|
-
*/
|
|
195
|
-
allowUnvalidatedSmallCluster?: boolean;
|
|
196
|
-
};
|
|
197
|
-
|
|
198
182
|
/** Override libp2p listen multiaddrs. */
|
|
199
183
|
listenAddrs?: string[];
|
|
184
|
+
/**
|
|
185
|
+
* Multiaddrs to advertise INSTEAD OF the listen addrs. For a node behind a NAT / reverse proxy /
|
|
186
|
+
* DNS front that binds one address but is reachable at another. When non-empty these REPLACE the
|
|
187
|
+
* advertised set entirely — observed/relayed addresses and {@link NodeOptions.appendAnnounceAddrs}
|
|
188
|
+
* are all dropped from it. An empty array means "unset" (libp2p's own semantics).
|
|
189
|
+
*/
|
|
190
|
+
announceAddrs?: string[];
|
|
191
|
+
/**
|
|
192
|
+
* Multiaddrs to advertise IN ADDITION TO the listen addrs. Ignored while
|
|
193
|
+
* {@link NodeOptions.announceAddrs} is non-empty.
|
|
194
|
+
*/
|
|
195
|
+
appendAnnounceAddrs?: string[];
|
|
200
196
|
/** Override libp2p transports. */
|
|
201
197
|
transports?: Libp2pTransports;
|
|
202
198
|
|
|
@@ -305,9 +301,10 @@ export type NodeOptions = {
|
|
|
305
301
|
* dialing peer's `PeerId.toString()`. See {@link AuthorizeInboundStream} and
|
|
306
302
|
* `docs/internals.md` § Inbound Stream Authorization.
|
|
307
303
|
*
|
|
308
|
-
* NOTE: this covers the four database protocols only. The reactivity, matchmaking,
|
|
304
|
+
* NOTE: this covers the four database protocols only. The dispute, reactivity, matchmaking,
|
|
309
305
|
* cohort-topic and libp2p built-in (identify/ping/…) protocols this node also registers are
|
|
310
|
-
* NOT gated by it.
|
|
306
|
+
* NOT gated by it. To refuse a peer at the connection level instead — every protocol at once,
|
|
307
|
+
* including identify — use {@link NodeOptions.connectionGater}.
|
|
311
308
|
*/
|
|
312
309
|
authorizeInboundStream?: AuthorizeInboundStream;
|
|
313
310
|
|
|
@@ -464,8 +461,13 @@ export async function createLibp2pNodeBase(
|
|
|
464
461
|
const libp2pOptions: Libp2pInit = {
|
|
465
462
|
start: false,
|
|
466
463
|
privateKey: nodePrivateKey,
|
|
464
|
+
// NOTE: libp2p's `AddressManagerInit` also carries `noAnnounce` and `announceFilter`; neither is
|
|
465
|
+
// exposed on `NodeOptions`. Add them here the same way if a deployment ever needs to suppress a
|
|
466
|
+
// specific advertised address rather than replace the whole set.
|
|
467
467
|
addresses: {
|
|
468
|
-
listen: listenAddrs
|
|
468
|
+
listen: listenAddrs,
|
|
469
|
+
...(options.announceAddrs ? { announce: options.announceAddrs } : {}),
|
|
470
|
+
...(options.appendAnnounceAddrs ? { appendAnnounce: options.appendAnnounceAddrs } : {})
|
|
469
471
|
},
|
|
470
472
|
connectionManager: {
|
|
471
473
|
// `autoDial`, `minConnections`, and `dialQueue` were stale libp2p option keys silently
|
|
@@ -603,7 +605,9 @@ export async function createLibp2pNodeBase(
|
|
|
603
605
|
});
|
|
604
606
|
return serviceFactory({
|
|
605
607
|
registrar: components.registrar,
|
|
606
|
-
repo: storageRepo
|
|
608
|
+
repo: storageRepo,
|
|
609
|
+
// So this service's authorization denials reach the same error sink as the other three.
|
|
610
|
+
logger: components.logger
|
|
607
611
|
});
|
|
608
612
|
},
|
|
609
613
|
|
|
@@ -696,21 +700,11 @@ export async function createLibp2pNodeBase(
|
|
|
696
700
|
const partitionDetector = new PartitionDetector();
|
|
697
701
|
const fretSvc = (node as any).services?.fret as FretService | undefined;
|
|
698
702
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5,
|
|
705
|
-
// Fail closed by default (an undersized cluster with no confident network-size estimate is
|
|
706
|
-
// rejected); embedders running knowingly-small meshes opt in through clusterPolicy.
|
|
707
|
-
allowUnvalidatedSmallCluster: options.clusterPolicy?.allowUnvalidatedSmallCluster ?? false,
|
|
708
|
-
partitionDetectionWindow: 60000,
|
|
709
|
-
// Configured full cluster size — the member's own reference for "full size" in the membership
|
|
710
|
-
// admission gate (a below-full-size declared set under low FRET confidence is refused as a possible
|
|
711
|
-
// self-shrink). Matches the size threaded into the coordinator below.
|
|
712
|
-
clusterSize: options.clusterSize ?? 10
|
|
713
|
-
};
|
|
703
|
+
// Every cluster-policy default lives in `cluster/cluster-policy.ts` — including WHY the admission
|
|
704
|
+
// gate and the repair corroboration floor resolve the one operator field
|
|
705
|
+
// (`clusterPolicy.assumedClusterSize`) to different values when it is absent. Extracted so those
|
|
706
|
+
// defaults can be asserted without booting a node.
|
|
707
|
+
const consensusConfig = resolveClusterPolicy(options);
|
|
714
708
|
|
|
715
709
|
// Fetch a block archive from one cohort peer over the sync protocol, bounded by a
|
|
716
710
|
// per-peer timeout so an unreachable peer can't stall reconciliation. Mirrors the
|
|
@@ -741,12 +735,17 @@ export async function createLibp2pNodeBase(
|
|
|
741
735
|
// (cohort drift, or a refused `missing-base-revision` commit). See `reconcile-block.ts` for
|
|
742
736
|
// the corroboration rules — in particular why both quorums are capped by how many peers
|
|
743
737
|
// could answer at all, which is what lets a genuinely two-node cohort heal.
|
|
738
|
+
// NOTE: this and the CoordinatorRepo below must cap against the SAME
|
|
739
|
+
// repairCorroborationClusterSize, or the two restoration paths disagree about how much trust a
|
|
740
|
+
// lone peer gets. Safe today because both read the one `resolveClusterPolicy` result above; if
|
|
741
|
+
// either ever resolves its own value, add a fail-fast coupling check like
|
|
742
|
+
// `assertSuperMajorityCoupling` rather than relying on proximity.
|
|
744
743
|
const reconcileBlock: ReconcileBlockCallback = createReconcileBlock({
|
|
745
744
|
selfPeerId: node.peerId.toString(),
|
|
746
745
|
fetchArchive: fetchArchiveFromPeer,
|
|
747
746
|
saveReplicatedBlock: (blockId, block, source) => storageRepo.saveReplicatedBlock(blockId, block, source),
|
|
748
747
|
simpleMajorityThreshold: consensusConfig.simpleMajorityThreshold,
|
|
749
|
-
|
|
748
|
+
repairCorroborationClusterSize: consensusConfig.repairCorroborationClusterSize,
|
|
750
749
|
reputation
|
|
751
750
|
});
|
|
752
751
|
|
|
@@ -803,12 +802,20 @@ export async function createLibp2pNodeBase(
|
|
|
803
802
|
options.transactionStateStore
|
|
804
803
|
);
|
|
805
804
|
|
|
806
|
-
// Create callback for querying cluster peers for their latest block revision
|
|
805
|
+
// Create callback for querying cluster peers for their latest block revision. Three-way
|
|
806
|
+
// contract (see ClusterLatestCallback): an ActionRev is the peer's claim, a resolved
|
|
807
|
+
// `undefined` is the peer answering "I hold nothing", and a REJECTION is silence — the
|
|
808
|
+
// coordinator counts it as "did not answer" and refuses to report an authoritative absent
|
|
809
|
+
// over it. Transport errors must therefore propagate, not collapse into `undefined` (that
|
|
810
|
+
// collapse let a slow two-node cohort report a missing block as authoritatively absent —
|
|
811
|
+
// ticket cluster-read-consult-cannot-report-unreachable).
|
|
807
812
|
const clusterLatestCallback: ClusterLatestCallback = async (peerId, blockId, context?) => {
|
|
808
813
|
// Self-read short-circuit: dialling self via SyncClient is a round trip
|
|
809
814
|
// with no remote on the other end, and on nodes without listen addresses
|
|
810
815
|
// (solo WebSocket-only, bare-RN, etc.) the self-dial can hang the dial
|
|
811
|
-
// queue. Read directly from the local storage repo instead.
|
|
816
|
+
// queue. Read directly from the local storage repo instead. The catch stays:
|
|
817
|
+
// a local storage error is not a cohort peer being unreachable, and the
|
|
818
|
+
// coordinator ignores a self rejection anyway.
|
|
812
819
|
if (peerId.equals(node.peerId)) {
|
|
813
820
|
try {
|
|
814
821
|
const result = await storageRepo.get({ blockIds: [blockId], context });
|
|
@@ -818,21 +825,22 @@ export async function createLibp2pNodeBase(
|
|
|
818
825
|
}
|
|
819
826
|
}
|
|
820
827
|
const syncClient = new SyncClient(peerId, keyNetwork, protocolPrefix);
|
|
821
|
-
try
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
828
|
+
// No try/catch: a dial or protocol failure rejects through to the coordinator, whose
|
|
829
|
+
// per-peer deadline also bounds a hung request — slowness needs no race here.
|
|
830
|
+
const response = await syncClient.requestBlock({ blockId, rev: undefined });
|
|
831
|
+
if (response.success && response.archive) {
|
|
832
|
+
const revisions = Object.keys(response.archive.revisions).map(Number);
|
|
833
|
+
if (revisions.length > 0) {
|
|
834
|
+
const maxRev = Math.max(...revisions);
|
|
835
|
+
const revisionData = response.archive.revisions[maxRev];
|
|
836
|
+
if (revisionData?.action) {
|
|
837
|
+
return { actionId: revisionData.action.actionId, rev: maxRev };
|
|
831
838
|
}
|
|
832
839
|
}
|
|
833
|
-
} catch {
|
|
834
|
-
// Peer may be unreachable - return undefined to skip this peer
|
|
835
840
|
}
|
|
841
|
+
// The peer DID answer, without data: `success:false` is the sync service's "Block not
|
|
842
|
+
// found in local storage", and an archive with no usable revisions holds nothing either
|
|
843
|
+
// way. Both are absent claims, not silence.
|
|
836
844
|
return undefined;
|
|
837
845
|
};
|
|
838
846
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { peerIdFromString } from "@libp2p/peer-id";
|
|
2
|
-
import type { ClusterRecord, IKeyNetwork, RepoMessage, BlockId, ClusterPeers, MessageOptions, ClusterConsensusConfig } from "@optimystic/db-core";
|
|
2
|
+
import type { ClusterRecord, IKeyNetwork, RepoMessage, BlockId, ClusterPeers, MessageOptions, ClusterConsensusConfig, ICluster } from "@optimystic/db-core";
|
|
3
3
|
import { CURRENT_MEMBERSHIP_VERSION, computeClusterMessageHash, membershipDigest } from "@optimystic/db-core";
|
|
4
|
-
import { ClusterClient } from "../cluster/client.js";
|
|
5
4
|
import { Pending } from "@optimystic/db-core";
|
|
6
5
|
import type { PeerId } from "@libp2p/interface";
|
|
7
6
|
import { createLogger, verbose } from '../logger.js'
|
|
@@ -13,6 +12,26 @@ import type { ITransactionStateStore } from "../cluster/i-transaction-state-stor
|
|
|
13
12
|
|
|
14
13
|
const log = createLogger('cluster')
|
|
15
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Consensus refused a transaction: enough members voted reject that super-majority became
|
|
17
|
+
* impossible. A typed error (rather than a bare `Error`) so the repo layer above can distinguish
|
|
18
|
+
* "the cluster voted this down" from transport/availability failures WITHOUT string-matching the
|
|
19
|
+
* rejection reasons — those are free-form text that is part of each member's signed vote payload
|
|
20
|
+
* (see cluster-repo's `computeSigningPayload`), so their wording must never become control flow.
|
|
21
|
+
* `CoordinatorRepo.pend` uses this to decide whether a rejection is a retryable stale-revision
|
|
22
|
+
* loss (confirmed against local storage) or a genuine validation fault.
|
|
23
|
+
*/
|
|
24
|
+
export class ValidatorRejectionError extends Error {
|
|
25
|
+
constructor(
|
|
26
|
+
message: string,
|
|
27
|
+
/** Per-peer reject reasons, verbatim from the vote signatures (free-form, wire-visible). */
|
|
28
|
+
readonly rejectReasons: Record<string, string>
|
|
29
|
+
) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'ValidatorRejectionError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
/** Cancel handle for an injected timer; cancels a not-yet-fired timer (safe no-op after fire/cancel). */
|
|
17
36
|
export type TimerCancel = () => void;
|
|
18
37
|
|
|
@@ -76,7 +95,8 @@ export class ClusterCoordinator {
|
|
|
76
95
|
|
|
77
96
|
constructor(
|
|
78
97
|
private readonly keyNetwork: IKeyNetwork,
|
|
79
|
-
|
|
98
|
+
/** Factory for a per-peer cluster RPC handle; only `update` is ever called, hence `ICluster`. */
|
|
99
|
+
private readonly createClusterClient: (peerId: PeerId) => ICluster,
|
|
80
100
|
private readonly cfg: ClusterConsensusConfig & { clusterSize: number },
|
|
81
101
|
private readonly localCluster?: {
|
|
82
102
|
update: (record: ClusterRecord) => Promise<ClusterRecord>;
|
|
@@ -322,9 +342,11 @@ export class ClusterCoordinator {
|
|
|
322
342
|
// If more than (peerCount - superMajority) nodes reject, we can never reach super-majority
|
|
323
343
|
const maxAllowedRejections = peerCount - superMajority;
|
|
324
344
|
if (rejectionCount > maxAllowedRejections) {
|
|
325
|
-
const
|
|
345
|
+
const rejectReasonsByPeer = Object.fromEntries(Object.entries(promises)
|
|
326
346
|
.filter(([_, sig]) => sig.type === 'reject')
|
|
327
|
-
.map(([peerId, sig]) =>
|
|
347
|
+
.map(([peerId, sig]) => [peerId, sig.rejectReason ?? 'unknown']));
|
|
348
|
+
const rejectReasons = Object.entries(rejectReasonsByPeer)
|
|
349
|
+
.map(([peerId, reason]) => `${peerId}: ${reason}`)
|
|
328
350
|
.join('; ');
|
|
329
351
|
log('cluster-tx:rejected-by-validators', {
|
|
330
352
|
messageHash: record.messageHash,
|
|
@@ -334,7 +356,9 @@ export class ClusterCoordinator {
|
|
|
334
356
|
reasons: rejectReasons
|
|
335
357
|
});
|
|
336
358
|
this.updateTransactionRecord(promised.record, 'rejected-by-validators');
|
|
337
|
-
throw new
|
|
359
|
+
throw new ValidatorRejectionError(
|
|
360
|
+
`Transaction rejected by validators (${rejectionCount}/${peerCount} rejected): ${rejectReasons}`,
|
|
361
|
+
rejectReasonsByPeer);
|
|
338
362
|
}
|
|
339
363
|
|
|
340
364
|
if (peerCount > 1 && approvalCount < superMajority) {
|