@optimystic/db-core 0.14.0 → 0.16.2
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/blocks/block-store.d.ts +18 -1
- package/dist/src/blocks/block-store.d.ts.map +1 -1
- package/dist/src/blocks/helpers.d.ts +2 -2
- package/dist/src/blocks/helpers.d.ts.map +1 -1
- package/dist/src/blocks/helpers.js +2 -2
- package/dist/src/blocks/helpers.js.map +1 -1
- package/dist/src/btree/btree.d.ts +27 -6
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +104 -61
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/chain/chain.d.ts.map +1 -1
- package/dist/src/chain/chain.js +11 -1
- package/dist/src/chain/chain.js.map +1 -1
- package/dist/src/cluster/index.d.ts +1 -0
- package/dist/src/cluster/index.d.ts.map +1 -1
- package/dist/src/cluster/index.js +1 -0
- package/dist/src/cluster/index.js.map +1 -1
- package/dist/src/cluster/membership.d.ts +49 -0
- package/dist/src/cluster/membership.d.ts.map +1 -0
- package/dist/src/cluster/membership.js +75 -0
- package/dist/src/cluster/membership.js.map +1 -0
- package/dist/src/cluster/structs.d.ts +50 -1
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +9 -1
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/cohort-topic/addressing.d.ts +8 -2
- package/dist/src/cohort-topic/addressing.d.ts.map +1 -1
- package/dist/src/cohort-topic/addressing.js +11 -3
- package/dist/src/cohort-topic/addressing.js.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts +13 -7
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.js +17 -6
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.js.map +1 -1
- package/dist/src/cohort-topic/antidos/replay-guard.d.ts +17 -0
- package/dist/src/cohort-topic/antidos/replay-guard.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/replay-guard.js +33 -0
- package/dist/src/cohort-topic/antidos/replay-guard.js.map +1 -1
- package/dist/src/cohort-topic/antidos/topic-budget.d.ts +7 -0
- package/dist/src/cohort-topic/antidos/topic-budget.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/topic-budget.js +8 -2
- package/dist/src/cohort-topic/antidos/topic-budget.js.map +1 -1
- package/dist/src/cohort-topic/coldstart.d.ts +34 -14
- package/dist/src/cohort-topic/coldstart.d.ts.map +1 -1
- package/dist/src/cohort-topic/coldstart.js +25 -11
- package/dist/src/cohort-topic/coldstart.js.map +1 -1
- package/dist/src/cohort-topic/gossip/bus.d.ts +12 -1
- package/dist/src/cohort-topic/gossip/bus.d.ts.map +1 -1
- package/dist/src/cohort-topic/gossip/bus.js +27 -1
- package/dist/src/cohort-topic/gossip/bus.js.map +1 -1
- package/dist/src/cohort-topic/gossip/view.d.ts +8 -0
- package/dist/src/cohort-topic/gossip/view.d.ts.map +1 -1
- package/dist/src/cohort-topic/gossip/view.js +17 -1
- package/dist/src/cohort-topic/gossip/view.js.map +1 -1
- package/dist/src/cohort-topic/member-engine.d.ts +10 -3
- package/dist/src/cohort-topic/member-engine.d.ts.map +1 -1
- package/dist/src/cohort-topic/member-engine.js +29 -14
- package/dist/src/cohort-topic/member-engine.js.map +1 -1
- package/dist/src/cohort-topic/membership/publisher.d.ts +14 -7
- package/dist/src/cohort-topic/membership/publisher.d.ts.map +1 -1
- package/dist/src/cohort-topic/membership/publisher.js +15 -27
- package/dist/src/cohort-topic/membership/publisher.js.map +1 -1
- package/dist/src/cohort-topic/membership/verifier.d.ts +35 -0
- package/dist/src/cohort-topic/membership/verifier.d.ts.map +1 -1
- package/dist/src/cohort-topic/membership/verifier.js +88 -3
- package/dist/src/cohort-topic/membership/verifier.js.map +1 -1
- package/dist/src/cohort-topic/promotion.d.ts +7 -0
- package/dist/src/cohort-topic/promotion.d.ts.map +1 -1
- package/dist/src/cohort-topic/promotion.js +16 -2
- package/dist/src/cohort-topic/promotion.js.map +1 -1
- package/dist/src/cohort-topic/registration/renewal.d.ts +18 -0
- package/dist/src/cohort-topic/registration/renewal.d.ts.map +1 -1
- package/dist/src/cohort-topic/registration/renewal.js +54 -0
- package/dist/src/cohort-topic/registration/renewal.js.map +1 -1
- package/dist/src/cohort-topic/registration/types.d.ts +12 -0
- package/dist/src/cohort-topic/registration/types.d.ts.map +1 -1
- package/dist/src/cohort-topic/registration/types.js +14 -0
- package/dist/src/cohort-topic/registration/types.js.map +1 -1
- package/dist/src/cohort-topic/service.d.ts +4 -3
- package/dist/src/cohort-topic/service.d.ts.map +1 -1
- package/dist/src/cohort-topic/service.js +38 -12
- package/dist/src/cohort-topic/service.js.map +1 -1
- package/dist/src/cohort-topic/sig/payloads.d.ts +23 -7
- package/dist/src/cohort-topic/sig/payloads.d.ts.map +1 -1
- package/dist/src/cohort-topic/sig/payloads.js +22 -6
- package/dist/src/cohort-topic/sig/payloads.js.map +1 -1
- package/dist/src/cohort-topic/traffic.d.ts +6 -0
- package/dist/src/cohort-topic/traffic.d.ts.map +1 -1
- package/dist/src/cohort-topic/traffic.js +16 -1
- package/dist/src/cohort-topic/traffic.js.map +1 -1
- package/dist/src/cohort-topic/walk.d.ts +22 -7
- package/dist/src/cohort-topic/walk.d.ts.map +1 -1
- package/dist/src/cohort-topic/walk.js +78 -18
- package/dist/src/cohort-topic/walk.js.map +1 -1
- package/dist/src/cohort-topic/wire/codec.d.ts +3 -1
- package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/codec.js +7 -1
- package/dist/src/cohort-topic/wire/codec.js.map +1 -1
- package/dist/src/cohort-topic/wire/index.d.ts +1 -1
- package/dist/src/cohort-topic/wire/index.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/index.js +1 -1
- package/dist/src/cohort-topic/wire/index.js.map +1 -1
- package/dist/src/cohort-topic/wire/payloads.d.ts +5 -4
- package/dist/src/cohort-topic/wire/payloads.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/payloads.js +14 -5
- package/dist/src/cohort-topic/wire/payloads.js.map +1 -1
- package/dist/src/cohort-topic/wire/primitives.d.ts +65 -0
- package/dist/src/cohort-topic/wire/primitives.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/primitives.js +173 -0
- package/dist/src/cohort-topic/wire/primitives.js.map +1 -0
- package/dist/src/cohort-topic/wire/types.d.ts +132 -5
- package/dist/src/cohort-topic/wire/types.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/validate.d.ts +15 -5
- package/dist/src/cohort-topic/wire/validate.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/validate.js +146 -123
- package/dist/src/cohort-topic/wire/validate.js.map +1 -1
- package/dist/src/collection/collection.d.ts +23 -12
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +111 -35
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collection/struct.d.ts +37 -2
- package/dist/src/collection/struct.d.ts.map +1 -1
- package/dist/src/collection/struct.js +16 -1
- package/dist/src/collection/struct.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +18 -1
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +36 -6
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/log/log.d.ts +26 -3
- package/dist/src/log/log.d.ts.map +1 -1
- package/dist/src/log/log.js +34 -7
- package/dist/src/log/log.js.map +1 -1
- package/dist/src/log/struct.d.ts +5 -4
- package/dist/src/log/struct.d.ts.map +1 -1
- package/dist/src/log/struct.js.map +1 -1
- package/dist/src/matchmaking/wire.d.ts +2 -1
- package/dist/src/matchmaking/wire.d.ts.map +1 -1
- package/dist/src/matchmaking/wire.js +13 -92
- package/dist/src/matchmaking/wire.js.map +1 -1
- package/dist/src/network/i-key-network.d.ts +9 -0
- package/dist/src/network/i-key-network.d.ts.map +1 -1
- package/dist/src/network/i-repo.d.ts +8 -0
- package/dist/src/network/i-repo.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +10 -0
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/reactivity/push-state.d.ts.map +1 -1
- package/dist/src/reactivity/push-state.js +4 -33
- package/dist/src/reactivity/push-state.js.map +1 -1
- package/dist/src/reactivity/wire-validate.d.ts +7 -22
- package/dist/src/reactivity/wire-validate.d.ts.map +1 -1
- package/dist/src/reactivity/wire-validate.js +7 -73
- package/dist/src/reactivity/wire-validate.js.map +1 -1
- package/dist/src/reactivity/wire.js +3 -3
- package/dist/src/reactivity/wire.js.map +1 -1
- package/dist/src/testing/async-wait.d.ts +23 -0
- package/dist/src/testing/async-wait.d.ts.map +1 -0
- package/dist/src/testing/async-wait.js +42 -0
- package/dist/src/testing/async-wait.js.map +1 -0
- package/dist/src/testing/index.d.ts +3 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +3 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/testing/test-transactor.d.ts +44 -0
- package/dist/src/testing/test-transactor.d.ts.map +1 -0
- package/dist/src/testing/test-transactor.js +423 -0
- package/dist/src/testing/test-transactor.js.map +1 -0
- package/dist/src/transaction/actions-engine.d.ts +6 -3
- package/dist/src/transaction/actions-engine.d.ts.map +1 -1
- package/dist/src/transaction/actions-engine.js +8 -7
- package/dist/src/transaction/actions-engine.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts +43 -22
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +443 -178
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/errors.d.ts +82 -0
- package/dist/src/transaction/errors.d.ts.map +1 -0
- package/dist/src/transaction/errors.js +93 -0
- package/dist/src/transaction/errors.js.map +1 -0
- package/dist/src/transaction/index.d.ts +6 -3
- package/dist/src/transaction/index.d.ts.map +1 -1
- package/dist/src/transaction/index.js +4 -2
- package/dist/src/transaction/index.js.map +1 -1
- package/dist/src/transaction/operations-hash.d.ts +119 -0
- package/dist/src/transaction/operations-hash.d.ts.map +1 -0
- package/dist/src/transaction/operations-hash.js +180 -0
- package/dist/src/transaction/operations-hash.js.map +1 -0
- package/dist/src/transaction/read-dependency-collector.d.ts +43 -0
- package/dist/src/transaction/read-dependency-collector.d.ts.map +1 -0
- package/dist/src/transaction/read-dependency-collector.js +69 -0
- package/dist/src/transaction/read-dependency-collector.js.map +1 -0
- package/dist/src/transaction/session.d.ts +12 -3
- package/dist/src/transaction/session.d.ts.map +1 -1
- package/dist/src/transaction/session.js +35 -9
- package/dist/src/transaction/session.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +162 -1
- package/dist/src/transaction/transaction.d.ts.map +1 -1
- package/dist/src/transaction/transaction.js +85 -3
- package/dist/src/transaction/transaction.js.map +1 -1
- package/dist/src/transaction/validator.d.ts +3 -11
- package/dist/src/transaction/validator.d.ts.map +1 -1
- package/dist/src/transaction/validator.js +69 -28
- package/dist/src/transaction/validator.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts +35 -0
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +146 -40
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +19 -5
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +43 -11
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/atomic-proxy.d.ts +35 -5
- package/dist/src/transform/atomic-proxy.d.ts.map +1 -1
- package/dist/src/transform/atomic-proxy.js +39 -7
- package/dist/src/transform/atomic-proxy.js.map +1 -1
- package/dist/src/transform/cache-source.d.ts +36 -5
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +84 -8
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +8 -2
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts +17 -2
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +66 -11
- package/dist/src/transform/tracker.js.map +1 -1
- package/dist/src/utility/backoff.d.ts +48 -0
- package/dist/src/utility/backoff.d.ts.map +1 -0
- package/dist/src/utility/backoff.js +60 -0
- package/dist/src/utility/backoff.js.map +1 -0
- package/dist/src/utility/pending.d.ts +1 -0
- package/dist/src/utility/pending.d.ts.map +1 -1
- package/dist/src/utility/pending.js +3 -1
- package/dist/src/utility/pending.js.map +1 -1
- package/package.json +4 -4
- package/src/blocks/block-store.ts +19 -1
- package/src/blocks/helpers.ts +3 -3
- package/src/btree/btree.ts +108 -63
- package/src/chain/chain.ts +11 -1
- package/src/cluster/index.ts +1 -0
- package/src/cluster/membership.ts +85 -0
- package/src/cluster/structs.ts +51 -1
- package/src/cohort-topic/addressing.ts +12 -4
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +21 -9
- package/src/cohort-topic/antidos/replay-guard.ts +37 -0
- package/src/cohort-topic/antidos/topic-budget.ts +17 -2
- package/src/cohort-topic/coldstart.ts +48 -15
- package/src/cohort-topic/gossip/bus.ts +39 -2
- package/src/cohort-topic/gossip/view.ts +26 -1
- package/src/cohort-topic/member-engine.ts +36 -15
- package/src/cohort-topic/membership/publisher.ts +25 -33
- package/src/cohort-topic/membership/verifier.ts +119 -3
- package/src/cohort-topic/promotion.ts +23 -2
- package/src/cohort-topic/registration/renewal.ts +73 -1
- package/src/cohort-topic/registration/types.ts +15 -0
- package/src/cohort-topic/service.ts +41 -15
- package/src/cohort-topic/sig/payloads.ts +29 -9
- package/src/cohort-topic/traffic.ts +23 -1
- package/src/cohort-topic/walk.ts +95 -24
- package/src/cohort-topic/wire/codec.ts +12 -0
- package/src/cohort-topic/wire/index.ts +2 -0
- package/src/cohort-topic/wire/payloads.ts +14 -5
- package/src/cohort-topic/wire/primitives.ts +188 -0
- package/src/cohort-topic/wire/types.ts +136 -4
- package/src/cohort-topic/wire/validate.ts +168 -134
- package/src/collection/collection.ts +123 -40
- package/src/collection/struct.ts +44 -2
- package/src/collections/tree/tree.ts +42 -5
- package/src/index.ts +1 -0
- package/src/log/log.ts +37 -9
- package/src/log/struct.ts +5 -4
- package/src/matchmaking/wire.ts +25 -102
- package/src/network/i-key-network.ts +10 -0
- package/src/network/i-repo.ts +8 -0
- package/src/network/struct.ts +10 -0
- package/src/reactivity/push-state.ts +4 -37
- package/src/reactivity/wire-validate.ts +7 -81
- package/src/reactivity/wire.ts +3 -3
- package/src/testing/async-wait.ts +65 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/test-transactor.ts +468 -0
- package/src/transaction/actions-engine.ts +8 -6
- package/src/transaction/coordinator.ts +498 -229
- package/src/transaction/errors.ts +91 -0
- package/src/transaction/index.ts +23 -3
- package/src/transaction/operations-hash.ts +196 -0
- package/src/transaction/read-dependency-collector.ts +78 -0
- package/src/transaction/session.ts +37 -9
- package/src/transaction/transaction.ts +211 -4
- package/src/transaction/validator.ts +73 -48
- package/src/transactor/network-transactor.ts +164 -49
- package/src/transactor/transactor-source.ts +47 -12
- package/src/transform/atomic-proxy.ts +92 -49
- package/src/transform/cache-source.ts +87 -9
- package/src/transform/helpers.ts +8 -2
- package/src/transform/tracker.ts +69 -11
- package/src/utility/backoff.ts +95 -0
- package/src/utility/pending.ts +3 -1
- package/src/transaction/context.ts +0 -103
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomBytes } from "@noble/hashes/utils.js";
|
|
2
|
+
import { toString as uint8ArrayToString } from "uint8arrays/to-string";
|
|
1
3
|
import type { BlockId } from "../blocks/index.js";
|
|
2
4
|
import type { CollectionId } from "../collection/index.js";
|
|
3
5
|
import { hashString } from "../utility/hash-string.js";
|
|
@@ -24,6 +26,17 @@ export type TransactionStamp = {
|
|
|
24
26
|
/** Absolute ms epoch after which transaction is invalid */
|
|
25
27
|
expiration: number;
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Random per-transaction nonce, folded into {@link TransactionStamp.id}.
|
|
31
|
+
*
|
|
32
|
+
* Anti-replay for read-free transactions: a transaction with no reads has nothing
|
|
33
|
+
* binding it to a point in history beyond {@link expiration}, so without this a
|
|
34
|
+
* captured signed read-free transaction could be re-submitted within the TTL window.
|
|
35
|
+
* The nonce makes two otherwise-identical transactions from the same peer at the
|
|
36
|
+
* same millisecond produce different ids (hence different signed bytes).
|
|
37
|
+
*/
|
|
38
|
+
nonce: string;
|
|
39
|
+
|
|
27
40
|
/** Hash of the stamp fields (computed) - stable identifier throughout transaction */
|
|
28
41
|
id: string;
|
|
29
42
|
};
|
|
@@ -52,6 +65,44 @@ export type Transaction = {
|
|
|
52
65
|
* Final transaction identity, used in logs
|
|
53
66
|
*/
|
|
54
67
|
id: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Optional client signature (base64url) over {@link clientSignaturePayload}
|
|
71
|
+
* `(stamp.id, statements, reads)`, produced at commit by an injected
|
|
72
|
+
* {@link TransactionSigner}.
|
|
73
|
+
*
|
|
74
|
+
* A node that wires a {@link ClientSignatureVerifier} into its
|
|
75
|
+
* {@link TransactionValidator} verifies this at pend; where no verifier is wired,
|
|
76
|
+
* unsigned transactions are still accepted (migration / single-node-dev posture).
|
|
77
|
+
* Plain field on the transaction, so it rides along through serialization/persistence
|
|
78
|
+
* for later recovery-time re-verification.
|
|
79
|
+
*
|
|
80
|
+
* NOTE: preservation is verified only through JSON round-trip and an in-memory session commit —
|
|
81
|
+
* NOT yet through the db-p2p cluster-repo on-disk transaction-log serialization. No recovery
|
|
82
|
+
* reader exists today; when one lands, confirm this field survives that path so `verifyPeerSig`
|
|
83
|
+
* can re-run at recovery time.
|
|
84
|
+
*/
|
|
85
|
+
signature?: string;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Aged, advisory retry priority (default 0 when absent). Rises by one per failed retry
|
|
89
|
+
* attempt (attempt-count-derived — deterministic, no wall clock, clock-skew-free) and is
|
|
90
|
+
* capped at {@link MaxPriority}. A cluster member uses it ONLY as the first tiebreak in a
|
|
91
|
+
* concurrent race between two simultaneously-pending conflicting transactions
|
|
92
|
+
* (`resolveRace`), so a transaction that keeps losing races eventually out-ranks fresh
|
|
93
|
+
* (priority-0) rivals instead of losing at random forever.
|
|
94
|
+
*
|
|
95
|
+
* FAIRNESS-ONLY — MUST NOT influence validity, the operations hash, stale-read checks, the
|
|
96
|
+
* transaction id, or the client signature. It is deliberately excluded from
|
|
97
|
+
* {@link createTransactionId} and {@link clientSignaturePayload} so two attempts of the same
|
|
98
|
+
* logical transaction (differing only in priority) keep the same id and signature; folding it
|
|
99
|
+
* into either would churn identity/anti-replay on every retry. It rides inside the signed
|
|
100
|
+
* cluster `message` (via the pend operation), so it is integrity-protected in transit — a
|
|
101
|
+
* relaying peer cannot strip or inflate it without invalidating the message hash — but the
|
|
102
|
+
* originating coordinator can still self-assert it (a fairness concern, not a safety one; see
|
|
103
|
+
* `docs/correctness.md` Theorem 9).
|
|
104
|
+
*/
|
|
105
|
+
priority?: number;
|
|
55
106
|
};
|
|
56
107
|
|
|
57
108
|
/**
|
|
@@ -73,11 +124,58 @@ export type TransactionRef = string; // The transaction ID
|
|
|
73
124
|
/** Default transaction time-to-live in milliseconds (30 seconds). */
|
|
74
125
|
export const DEFAULT_TRANSACTION_TTL_MS = 30_000;
|
|
75
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Upper bound on a transaction's aged {@link Transaction.priority}. Capping is essential: it
|
|
129
|
+
* bounds the fairness effect (an aged transaction cannot accrue unlimited advantage) and bounds
|
|
130
|
+
* the throughput cost (it can impose at most `MaxPriority` extra race-losses on rivals before it
|
|
131
|
+
* commits). Two transactions capped out at `MaxPriority` fall back to the existing promise-count /
|
|
132
|
+
* message-hash tiebreak, so behavior degrades gracefully to the pre-priority status quo.
|
|
133
|
+
*/
|
|
134
|
+
export const MaxPriority = 8;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Clamp an attempt-derived priority into `[0, MaxPriority]`. Total: any non-finite / negative
|
|
138
|
+
* input collapses to 0, anything above the cap to {@link MaxPriority}, and the result is floored
|
|
139
|
+
* to an integer. Used both where priority is *set* (the retry loops) and where it is *read* (a
|
|
140
|
+
* cluster member's `resolveRace`), so a Byzantine coordinator self-asserting an out-of-range value
|
|
141
|
+
* cannot exceed the cap.
|
|
142
|
+
*/
|
|
143
|
+
export function clampPriority(priority: number | undefined): number {
|
|
144
|
+
if (typeof priority !== 'number' || Number.isNaN(priority) || priority <= 0) {
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
// A positive out-of-range value (including +Infinity, or a Byzantine self-asserted number) clamps
|
|
148
|
+
// DOWN to the cap; anything strictly inside the range is floored to an integer.
|
|
149
|
+
if (priority >= MaxPriority) {
|
|
150
|
+
return MaxPriority;
|
|
151
|
+
}
|
|
152
|
+
return Math.floor(priority);
|
|
153
|
+
}
|
|
154
|
+
|
|
76
155
|
/** Check whether a transaction stamp has expired. */
|
|
77
156
|
export function isTransactionExpired(stamp: TransactionStamp): boolean {
|
|
78
157
|
return Date.now() > stamp.expiration;
|
|
79
158
|
}
|
|
80
159
|
|
|
160
|
+
/** The stamp fields that feed the id hash (everything except the id itself). */
|
|
161
|
+
export type StampFields = Omit<TransactionStamp, 'id'>;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Compute the canonical stamp id from its fields. MUST match the historical field order
|
|
165
|
+
* (peerId, timestamp, schemaHash, engineId, expiration, nonce) — changing it churns every
|
|
166
|
+
* existing stamp id and client signature. Both {@link createTransactionStamp} and the
|
|
167
|
+
* validator's integrity check derive the id through this one function, so a validating node
|
|
168
|
+
* can re-derive the id and reject a stamp whose fields were tampered while id stayed fixed.
|
|
169
|
+
*/
|
|
170
|
+
export async function computeStampId(fields: StampFields): Promise<string> {
|
|
171
|
+
// Destructure and rebuild in fixed order rather than passing `fields` straight to
|
|
172
|
+
// JSON.stringify — this ignores any stray `id` (or extra) property a caller slipped in,
|
|
173
|
+
// so the serialized bytes depend on exactly the six fields and nothing else.
|
|
174
|
+
const { peerId, timestamp, schemaHash, engineId, expiration, nonce } = fields;
|
|
175
|
+
const stampData = JSON.stringify({ peerId, timestamp, schemaHash, engineId, expiration, nonce });
|
|
176
|
+
return `stamp:${await hashString(stampData)}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
81
179
|
/**
|
|
82
180
|
* Create a transaction stamp with computed id.
|
|
83
181
|
* The id is a hash of the stamp fields (including expiration).
|
|
@@ -90,14 +188,27 @@ export async function createTransactionStamp(
|
|
|
90
188
|
ttlMs: number = DEFAULT_TRANSACTION_TTL_MS
|
|
91
189
|
): Promise<TransactionStamp> {
|
|
92
190
|
const expiration = timestamp + ttlMs;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
191
|
+
// Cross-platform CSPRNG (Node + browser) via @noble/hashes — matches the rest of
|
|
192
|
+
// db-core (see collection.ts, transactor-source.ts). The nonce is folded into the
|
|
193
|
+
// id hash so two otherwise-identical stamps diverge.
|
|
194
|
+
const nonce = uint8ArrayToString(randomBytes(16), 'base64url');
|
|
195
|
+
const id = await computeStampId({ peerId, timestamp, schemaHash, engineId, expiration, nonce });
|
|
196
|
+
return { peerId, timestamp, schemaHash, engineId, expiration, nonce, id };
|
|
96
197
|
}
|
|
97
198
|
|
|
98
199
|
/**
|
|
99
200
|
* Create a transaction id from stamp id, statements, and reads.
|
|
100
201
|
* This is the final transaction identity used in logs.
|
|
202
|
+
*
|
|
203
|
+
* NOTE: reads are serialized in their given order here (non-canonical). This differs
|
|
204
|
+
* from {@link clientSignaturePayload}, which canonicalises reads before hashing. That is
|
|
205
|
+
* intentional for this ticket — the signature payload is self-contained — but the
|
|
206
|
+
* non-canonical ordering here is a separate hygiene concern (see
|
|
207
|
+
* design-consensus-hygiene-notes), NOT fixed here to avoid changing existing tx ids.
|
|
208
|
+
*
|
|
209
|
+
* NOTE: {@link Transaction.priority} is deliberately absent from this preimage — the id is a pure
|
|
210
|
+
* function of `(stampId, statements, reads)`, so two retry attempts that differ only in aged
|
|
211
|
+
* priority produce the SAME id. Do not add priority here.
|
|
101
212
|
*/
|
|
102
213
|
export async function createTransactionId(
|
|
103
214
|
stampId: string,
|
|
@@ -108,6 +219,55 @@ export async function createTransactionId(
|
|
|
108
219
|
return `tx:${await hashString(txData)}`;
|
|
109
220
|
}
|
|
110
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Version prefix for the client-signature payload, so the signed form can evolve
|
|
224
|
+
* without ambiguity. Distinct from (but consistent in spirit with) the operations-hash
|
|
225
|
+
* canonical form — this covers transaction INPUTS, not operations.
|
|
226
|
+
*/
|
|
227
|
+
export const CLIENT_SIG_VERSION = 'txsig:v1';
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Canonical bytes a client signs and a node verifies. Deterministic: reads are sorted
|
|
231
|
+
* (blockId, then revision); statements keep their sequential order.
|
|
232
|
+
*
|
|
233
|
+
* Binds three things at once: the CLIENT IDENTITY (via `stampId`, which hashes peerId +
|
|
234
|
+
* nonce), the EXACT statements, and the OCC read set (blockId + revision — "tail
|
|
235
|
+
* binding" for anti-replay). Both signer and verifier derive the bytes from
|
|
236
|
+
* `transaction.stamp.id` + `transaction.statements` + `transaction.reads` through this
|
|
237
|
+
* one function, so they reproduce identical bytes regardless of read ordering.
|
|
238
|
+
*
|
|
239
|
+
* NOTE: {@link Transaction.priority} is deliberately absent from the signed bytes — a retry that
|
|
240
|
+
* only bumps priority keeps the same signature, so anti-replay / OCC binding never churns. Do not
|
|
241
|
+
* add priority here.
|
|
242
|
+
*/
|
|
243
|
+
export function clientSignaturePayload(
|
|
244
|
+
stampId: string,
|
|
245
|
+
statements: readonly string[],
|
|
246
|
+
reads: readonly ReadDependency[]
|
|
247
|
+
): Uint8Array {
|
|
248
|
+
const canonicalReads = [...reads].sort(
|
|
249
|
+
(a, b) => a.blockId < b.blockId ? -1 : a.blockId > b.blockId ? 1
|
|
250
|
+
: a.revision - b.revision
|
|
251
|
+
);
|
|
252
|
+
const body = JSON.stringify({ stampId, statements, reads: canonicalReads });
|
|
253
|
+
return new TextEncoder().encode(`${CLIENT_SIG_VERSION}:${body}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Signs the canonical client-signature payload, returning base64url. Async to allow
|
|
258
|
+
* libp2p `PrivateKey.sign`; tests pass a synchronous fake.
|
|
259
|
+
*/
|
|
260
|
+
export type TransactionSigner = (payload: Uint8Array) => Promise<string> | string;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Returns true iff `signature` (base64url) is a valid client signature over `payload`
|
|
264
|
+
* for signer identity `peerId`. Total: returns false, never throws, on any malformed
|
|
265
|
+
* input. The p2p wiring backs this with verifyPeerSig + peerIdBindsPublicKey; tests
|
|
266
|
+
* pass a fake.
|
|
267
|
+
*/
|
|
268
|
+
export type ClientSignatureVerifier =
|
|
269
|
+
(peerId: string, payload: Uint8Array, signature: string) => boolean;
|
|
270
|
+
|
|
111
271
|
|
|
112
272
|
|
|
113
273
|
/**
|
|
@@ -118,8 +278,41 @@ export async function createTransactionId(
|
|
|
118
278
|
* 1. Parsing the engine-specific statements
|
|
119
279
|
* 2. Executing/re-executing to produce actions
|
|
120
280
|
* 3. Returning the resulting actions per collection
|
|
281
|
+
*
|
|
282
|
+
* CONTRACT — an engine must never cause the same action to be applied twice. The
|
|
283
|
+
* caller (a {@link TransactionSession} on the no-pre-supplied-actions branch, or a
|
|
284
|
+
* {@link TransactionCoordinator} in `execute()`) applies whatever non-empty
|
|
285
|
+
* `CollectionActions[]` this method RETURNS. Therefore `execute()` must satisfy
|
|
286
|
+
* exactly ONE of:
|
|
287
|
+
*
|
|
288
|
+
* (a) PURE TRANSLATOR (preferred; how {@link ActionsEngine} works) — parse the
|
|
289
|
+
* statements, RETURN the actions, and do NOT apply them / do NOT call
|
|
290
|
+
* `coordinator.applyActions` / do NOT mutate any collection state. The caller
|
|
291
|
+
* applies them exactly once.
|
|
292
|
+
*
|
|
293
|
+
* (b) SIDE-EFFECT APPLY — apply the actions itself while translating (e.g. the
|
|
294
|
+
* Quereus vtab path stages rows into the coordinator during `db.exec`) and then
|
|
295
|
+
* RETURN an EMPTY actions array, so the caller re-applies nothing.
|
|
296
|
+
*
|
|
297
|
+
* An engine that BOTH applies as a side effect AND returns those same actions
|
|
298
|
+
* double-applies (the caller re-applies the returned actions). Additionally, a
|
|
299
|
+
* side-effecting engine used for validation must apply only to the validator's
|
|
300
|
+
* ISOLATED coordinator — applying to the main coordinator leaks into the validating
|
|
301
|
+
* node's live state.
|
|
121
302
|
*/
|
|
122
303
|
export interface ITransactionEngine {
|
|
304
|
+
/**
|
|
305
|
+
* Stable identifier for this engine.
|
|
306
|
+
*
|
|
307
|
+
* This is the value stamped into {@link TransactionStamp.engineId} when a
|
|
308
|
+
* {@link TransactionSession} builds a transaction, and the key a
|
|
309
|
+
* {@link TransactionValidator} resolves the engine by. It MUST match the key the
|
|
310
|
+
* engine is registered under in the validator's `engines` map — otherwise a
|
|
311
|
+
* validating node rejects the transaction as `Unknown engine: <id>` before it
|
|
312
|
+
* ever re-executes.
|
|
313
|
+
*/
|
|
314
|
+
readonly id: string;
|
|
315
|
+
|
|
123
316
|
/**
|
|
124
317
|
* Process a transaction statements to produce actions.
|
|
125
318
|
*
|
|
@@ -145,6 +338,16 @@ export type ExecutionResult = {
|
|
|
145
338
|
results?: Map<CollectionId, any[]>;
|
|
146
339
|
/** Error message if execution failed */
|
|
147
340
|
error?: string;
|
|
341
|
+
/**
|
|
342
|
+
* On a PARTIAL multi-collection commit failure, the collections that DID durably
|
|
343
|
+
* commit through consensus before the failure (and thus CANNOT be rolled back —
|
|
344
|
+
* reconciliation is required). Absent/empty means nothing durably committed, so
|
|
345
|
+
* the caller may treat the failure as a clean abort. See
|
|
346
|
+
* {@link CoordinatorPartialCommitError} for the session-mode (commit) analog.
|
|
347
|
+
*/
|
|
348
|
+
committedCollections?: CollectionId[];
|
|
349
|
+
/** On a partial multi-collection commit failure, the collections that failed to commit. */
|
|
350
|
+
failedCollections?: CollectionId[];
|
|
148
351
|
};
|
|
149
352
|
|
|
150
353
|
/**
|
|
@@ -196,7 +399,11 @@ export interface ITransactionValidator {
|
|
|
196
399
|
/**
|
|
197
400
|
* Validate a transaction by re-executing and comparing operations hash.
|
|
198
401
|
*
|
|
199
|
-
* Validation steps:
|
|
402
|
+
* Validation steps (see TransactionValidator.validate for the authoritative order):
|
|
403
|
+
* 0.0. Verify stamp.id is the true hash of the stamp fields (integrity — runs first,
|
|
404
|
+
* since every later step trusts a stamp field)
|
|
405
|
+
* 0. Verify the stamp has not expired
|
|
406
|
+
* 0.5. Verify the client signature (only if a signature verifier is wired)
|
|
200
407
|
* 1. Verify stamp.engineId matches a known engine
|
|
201
408
|
* 2. Verify stamp.schemaHash matches local schema
|
|
202
409
|
* 3. Verify read dependencies (no stale reads)
|
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import type { BlockId, CollectionId,
|
|
2
|
-
import type { Transaction, ITransactionEngine, ITransactionValidator, ValidationResult, CollectionActions } from './transaction.js';
|
|
1
|
+
import type { BlockId, CollectionId, Transforms } from '../index.js';
|
|
2
|
+
import type { Transaction, ITransactionEngine, ITransactionValidator, ValidationResult, CollectionActions, ClientSignatureVerifier } from './transaction.js';
|
|
3
3
|
import type { BlockActionState } from '../network/struct.js';
|
|
4
|
-
import { isTransactionExpired } from './transaction.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Represents an operation on a block within a collection.
|
|
9
|
-
* Must match the Operation type in coordinator.ts for consistent hashing.
|
|
10
|
-
*/
|
|
11
|
-
type Operation =
|
|
12
|
-
| { readonly type: 'insert'; readonly collectionId: CollectionId; readonly blockId: BlockId; readonly block: IBlock }
|
|
13
|
-
| { readonly type: 'update'; readonly collectionId: CollectionId; readonly blockId: BlockId; readonly operations: BlockOperations }
|
|
14
|
-
| { readonly type: 'delete'; readonly collectionId: CollectionId; readonly blockId: BlockId };
|
|
4
|
+
import { isTransactionExpired, clientSignaturePayload, computeStampId } from './transaction.js';
|
|
5
|
+
import { collectOperations, hashOperations, opsHashVersion, OPS_HASH_VERSION } from './operations-hash.js';
|
|
15
6
|
|
|
16
7
|
/**
|
|
17
8
|
* Engine registration for validation.
|
|
@@ -52,13 +43,24 @@ export class TransactionValidator implements ITransactionValidator {
|
|
|
52
43
|
constructor(
|
|
53
44
|
private readonly engines: Map<string, EngineRegistration>,
|
|
54
45
|
private readonly createValidationCoordinator: ValidationCoordinatorFactory,
|
|
55
|
-
private readonly blockStateProvider?: BlockStateProvider
|
|
46
|
+
private readonly blockStateProvider?: BlockStateProvider,
|
|
47
|
+
private readonly verifyClientSignature?: ClientSignatureVerifier
|
|
56
48
|
) {}
|
|
57
49
|
|
|
58
50
|
async validate(transaction: Transaction, operationsHash: string): Promise<ValidationResult> {
|
|
59
51
|
const { stamp } = transaction;
|
|
60
52
|
|
|
61
|
-
// 0.
|
|
53
|
+
// 0.0. Integrity: the id must be the true hash of the stamp fields. Runs FIRST because
|
|
54
|
+
// every later step (expiration, signature, engine/schema selection) trusts a stamp field.
|
|
55
|
+
// Always-on (one SHA-256, cheap) and independent of whether a signature verifier is wired —
|
|
56
|
+
// a tampered stamp corrupts expiration and engine selection regardless of signatures.
|
|
57
|
+
const expectedId = await computeStampId(stamp);
|
|
58
|
+
if (expectedId !== stamp.id) {
|
|
59
|
+
return { valid: false, reason: 'Tampered transaction stamp' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 0. Check expiration before any other work. Ordered BEFORE the signature check so
|
|
63
|
+
// an attacker cannot learn signature-validity for an already-expired transaction.
|
|
62
64
|
if (isTransactionExpired(stamp)) {
|
|
63
65
|
return {
|
|
64
66
|
valid: false,
|
|
@@ -66,6 +68,19 @@ export class TransactionValidator implements ITransactionValidator {
|
|
|
66
68
|
};
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
// 0.5. Verify the client signature, if a verifier port is wired. When the port is
|
|
72
|
+
// absent (migration / single-node-dev posture) unsigned AND signed transactions
|
|
73
|
+
// both pass — the p2p enforcement flag decides whether to inject the port at all.
|
|
74
|
+
if (this.verifyClientSignature) {
|
|
75
|
+
if (transaction.signature === undefined) {
|
|
76
|
+
return { valid: false, reason: 'Missing client signature' };
|
|
77
|
+
}
|
|
78
|
+
const payload = clientSignaturePayload(stamp.id, transaction.statements, transaction.reads);
|
|
79
|
+
if (!this.verifyClientSignature(stamp.peerId, payload, transaction.signature)) {
|
|
80
|
+
return { valid: false, reason: 'Invalid client signature' };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
// 1. Verify engine exists
|
|
70
85
|
const registration = this.engines.get(stamp.engineId);
|
|
71
86
|
if (!registration) {
|
|
@@ -102,7 +117,24 @@ export class TransactionValidator implements ITransactionValidator {
|
|
|
102
117
|
const validationCoordinator = this.createValidationCoordinator();
|
|
103
118
|
|
|
104
119
|
try {
|
|
105
|
-
// 5. Re-execute transaction through engine
|
|
120
|
+
// 5. Re-execute transaction through engine.
|
|
121
|
+
//
|
|
122
|
+
// Which of the two ITransactionEngine models the registered engine uses decides
|
|
123
|
+
// where isolation comes from (see the contract in transaction.ts):
|
|
124
|
+
//
|
|
125
|
+
// (a) PURE TRANSLATOR (ActionsEngine) — execute() parses statements into
|
|
126
|
+
// actions and mutates NOTHING, so even an engine constructed against the
|
|
127
|
+
// MAIN coordinator cannot leak into main state here. The returned actions
|
|
128
|
+
// are applied once, on the isolated validationCoordinator at step 6.
|
|
129
|
+
//
|
|
130
|
+
// (b) SIDE-EFFECT APPLY (QuereusEngine) — execute() applies while translating
|
|
131
|
+
// (db.exec drives the vtab into coordinator.applyActions) and returns EMPTY
|
|
132
|
+
// actions. That application lands on whatever coordinator the engine/vtab is
|
|
133
|
+
// bound to, NOT on validationCoordinator, so isolation is the caller's job:
|
|
134
|
+
// the createValidationCoordinator wiring must bind/reset an isolated world
|
|
135
|
+
// (see quereus-validator, which resets the coordinator before each run and
|
|
136
|
+
// makes its validationCoordinator.applyActions a no-op). Step 6 is then
|
|
137
|
+
// skipped by the empty-actions guard.
|
|
106
138
|
const result = await registration.engine.execute(transaction);
|
|
107
139
|
if (!result.success) {
|
|
108
140
|
return {
|
|
@@ -111,20 +143,39 @@ export class TransactionValidator implements ITransactionValidator {
|
|
|
111
143
|
};
|
|
112
144
|
}
|
|
113
145
|
|
|
114
|
-
// 6. Apply actions to validation coordinator (builds transforms)
|
|
146
|
+
// 6. Apply actions to the isolated validation coordinator (builds transforms).
|
|
147
|
+
// Reached only for pure-translator engines that RETURNED actions; a side-effect
|
|
148
|
+
// engine returns empty and already applied during execute() (see above).
|
|
115
149
|
if (result.actions && result.actions.length > 0) {
|
|
116
150
|
await validationCoordinator.applyActions(result.actions, stamp.id);
|
|
117
151
|
}
|
|
118
152
|
|
|
119
153
|
// 7. Collect operations from validation coordinator
|
|
120
154
|
const transforms = validationCoordinator.getTransforms();
|
|
121
|
-
const allOperations = this.collectOperations(transforms);
|
|
122
155
|
|
|
123
|
-
// 8. Compute hash
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
156
|
+
// 8. Compute hash via the shared operations-hash module — the SAME collect +
|
|
157
|
+
// canonicalise + hash the coordinator ran, so an honest sender and this
|
|
158
|
+
// validator cannot diverge on ordering.
|
|
159
|
+
const computedHash = await hashOperations(collectOperations(transforms));
|
|
160
|
+
|
|
161
|
+
// 9. Compare with sender's hash. Split the failure into two distinct causes so
|
|
162
|
+
// version skew is diagnosable rather than looking like a content fault:
|
|
163
|
+
// - VERSION SKEW: the sender's token carries a format version this node does not
|
|
164
|
+
// produce (a foreign vN, a bare legacy `ops:`, or an unparseable token → null).
|
|
165
|
+
// These bytes are not even comparable, so surface a clear "unsupported format
|
|
166
|
+
// version" error. This is DETECTION only — the node cannot cross-compute the
|
|
167
|
+
// peer's format, so a mixed-version cluster fails legibly, not silently.
|
|
168
|
+
// - CONTENT MISMATCH: same format version, different bytes — a genuine operations
|
|
169
|
+
// disagreement (or a Byzantine lie); the existing error is unchanged.
|
|
127
170
|
if (computedHash !== operationsHash) {
|
|
171
|
+
const senderVersion = opsHashVersion(operationsHash);
|
|
172
|
+
if (senderVersion !== OPS_HASH_VERSION) {
|
|
173
|
+
return {
|
|
174
|
+
valid: false,
|
|
175
|
+
reason: `Unsupported operations-hash format version: local=${OPS_HASH_VERSION}, sender=${senderVersion ?? 'unrecognized'}`,
|
|
176
|
+
computedHash
|
|
177
|
+
};
|
|
178
|
+
}
|
|
128
179
|
return {
|
|
129
180
|
valid: false,
|
|
130
181
|
reason: `Operations hash mismatch`,
|
|
@@ -142,31 +193,5 @@ export class TransactionValidator implements ITransactionValidator {
|
|
|
142
193
|
const registration = this.engines.get(engineId);
|
|
143
194
|
return registration ? await registration.getSchemaHash() : undefined;
|
|
144
195
|
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Collect all operations from transforms.
|
|
148
|
-
*/
|
|
149
|
-
private collectOperations(transforms: Map<CollectionId, Transforms>): readonly Operation[] {
|
|
150
|
-
return Array.from(transforms.entries()).flatMap(([collectionId, t]) => [
|
|
151
|
-
...Object.entries(t.inserts ?? {}).map(([blockId, block]) =>
|
|
152
|
-
({ type: 'insert' as const, collectionId, blockId, block })
|
|
153
|
-
),
|
|
154
|
-
...Object.entries(t.updates ?? {}).map(([blockId, operations]) =>
|
|
155
|
-
({ type: 'update' as const, collectionId, blockId, operations })
|
|
156
|
-
),
|
|
157
|
-
...(t.deletes ?? []).map(blockId =>
|
|
158
|
-
({ type: 'delete' as const, collectionId, blockId })
|
|
159
|
-
)
|
|
160
|
-
]);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Compute hash of all operations.
|
|
165
|
-
* Must match TransactionCoordinator.hashOperations for consistent validation.
|
|
166
|
-
*/
|
|
167
|
-
private async hashOperations(operations: readonly Operation[]): Promise<string> {
|
|
168
|
-
const operationsData = JSON.stringify(operations);
|
|
169
|
-
return `ops:${await hashString(operationsData)}`;
|
|
170
|
-
}
|
|
171
196
|
}
|
|
172
197
|
|