@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.
Files changed (301) hide show
  1. package/dist/src/blocks/block-store.d.ts +18 -1
  2. package/dist/src/blocks/block-store.d.ts.map +1 -1
  3. package/dist/src/blocks/helpers.d.ts +2 -2
  4. package/dist/src/blocks/helpers.d.ts.map +1 -1
  5. package/dist/src/blocks/helpers.js +2 -2
  6. package/dist/src/blocks/helpers.js.map +1 -1
  7. package/dist/src/btree/btree.d.ts +27 -6
  8. package/dist/src/btree/btree.d.ts.map +1 -1
  9. package/dist/src/btree/btree.js +104 -61
  10. package/dist/src/btree/btree.js.map +1 -1
  11. package/dist/src/chain/chain.d.ts.map +1 -1
  12. package/dist/src/chain/chain.js +11 -1
  13. package/dist/src/chain/chain.js.map +1 -1
  14. package/dist/src/cluster/index.d.ts +1 -0
  15. package/dist/src/cluster/index.d.ts.map +1 -1
  16. package/dist/src/cluster/index.js +1 -0
  17. package/dist/src/cluster/index.js.map +1 -1
  18. package/dist/src/cluster/membership.d.ts +49 -0
  19. package/dist/src/cluster/membership.d.ts.map +1 -0
  20. package/dist/src/cluster/membership.js +75 -0
  21. package/dist/src/cluster/membership.js.map +1 -0
  22. package/dist/src/cluster/structs.d.ts +50 -1
  23. package/dist/src/cluster/structs.d.ts.map +1 -1
  24. package/dist/src/cluster/structs.js +9 -1
  25. package/dist/src/cluster/structs.js.map +1 -1
  26. package/dist/src/cohort-topic/addressing.d.ts +8 -2
  27. package/dist/src/cohort-topic/addressing.d.ts.map +1 -1
  28. package/dist/src/cohort-topic/addressing.js +11 -3
  29. package/dist/src/cohort-topic/addressing.js.map +1 -1
  30. package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts +13 -7
  31. package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts.map +1 -1
  32. package/dist/src/cohort-topic/antidos/bootstrap-evidence.js +17 -6
  33. package/dist/src/cohort-topic/antidos/bootstrap-evidence.js.map +1 -1
  34. package/dist/src/cohort-topic/antidos/replay-guard.d.ts +17 -0
  35. package/dist/src/cohort-topic/antidos/replay-guard.d.ts.map +1 -1
  36. package/dist/src/cohort-topic/antidos/replay-guard.js +33 -0
  37. package/dist/src/cohort-topic/antidos/replay-guard.js.map +1 -1
  38. package/dist/src/cohort-topic/antidos/topic-budget.d.ts +7 -0
  39. package/dist/src/cohort-topic/antidos/topic-budget.d.ts.map +1 -1
  40. package/dist/src/cohort-topic/antidos/topic-budget.js +8 -2
  41. package/dist/src/cohort-topic/antidos/topic-budget.js.map +1 -1
  42. package/dist/src/cohort-topic/coldstart.d.ts +34 -14
  43. package/dist/src/cohort-topic/coldstart.d.ts.map +1 -1
  44. package/dist/src/cohort-topic/coldstart.js +25 -11
  45. package/dist/src/cohort-topic/coldstart.js.map +1 -1
  46. package/dist/src/cohort-topic/gossip/bus.d.ts +12 -1
  47. package/dist/src/cohort-topic/gossip/bus.d.ts.map +1 -1
  48. package/dist/src/cohort-topic/gossip/bus.js +27 -1
  49. package/dist/src/cohort-topic/gossip/bus.js.map +1 -1
  50. package/dist/src/cohort-topic/gossip/view.d.ts +8 -0
  51. package/dist/src/cohort-topic/gossip/view.d.ts.map +1 -1
  52. package/dist/src/cohort-topic/gossip/view.js +17 -1
  53. package/dist/src/cohort-topic/gossip/view.js.map +1 -1
  54. package/dist/src/cohort-topic/member-engine.d.ts +10 -3
  55. package/dist/src/cohort-topic/member-engine.d.ts.map +1 -1
  56. package/dist/src/cohort-topic/member-engine.js +29 -14
  57. package/dist/src/cohort-topic/member-engine.js.map +1 -1
  58. package/dist/src/cohort-topic/membership/publisher.d.ts +14 -7
  59. package/dist/src/cohort-topic/membership/publisher.d.ts.map +1 -1
  60. package/dist/src/cohort-topic/membership/publisher.js +15 -27
  61. package/dist/src/cohort-topic/membership/publisher.js.map +1 -1
  62. package/dist/src/cohort-topic/membership/verifier.d.ts +35 -0
  63. package/dist/src/cohort-topic/membership/verifier.d.ts.map +1 -1
  64. package/dist/src/cohort-topic/membership/verifier.js +88 -3
  65. package/dist/src/cohort-topic/membership/verifier.js.map +1 -1
  66. package/dist/src/cohort-topic/promotion.d.ts +7 -0
  67. package/dist/src/cohort-topic/promotion.d.ts.map +1 -1
  68. package/dist/src/cohort-topic/promotion.js +16 -2
  69. package/dist/src/cohort-topic/promotion.js.map +1 -1
  70. package/dist/src/cohort-topic/registration/renewal.d.ts +18 -0
  71. package/dist/src/cohort-topic/registration/renewal.d.ts.map +1 -1
  72. package/dist/src/cohort-topic/registration/renewal.js +54 -0
  73. package/dist/src/cohort-topic/registration/renewal.js.map +1 -1
  74. package/dist/src/cohort-topic/registration/types.d.ts +12 -0
  75. package/dist/src/cohort-topic/registration/types.d.ts.map +1 -1
  76. package/dist/src/cohort-topic/registration/types.js +14 -0
  77. package/dist/src/cohort-topic/registration/types.js.map +1 -1
  78. package/dist/src/cohort-topic/service.d.ts +4 -3
  79. package/dist/src/cohort-topic/service.d.ts.map +1 -1
  80. package/dist/src/cohort-topic/service.js +38 -12
  81. package/dist/src/cohort-topic/service.js.map +1 -1
  82. package/dist/src/cohort-topic/sig/payloads.d.ts +23 -7
  83. package/dist/src/cohort-topic/sig/payloads.d.ts.map +1 -1
  84. package/dist/src/cohort-topic/sig/payloads.js +22 -6
  85. package/dist/src/cohort-topic/sig/payloads.js.map +1 -1
  86. package/dist/src/cohort-topic/traffic.d.ts +6 -0
  87. package/dist/src/cohort-topic/traffic.d.ts.map +1 -1
  88. package/dist/src/cohort-topic/traffic.js +16 -1
  89. package/dist/src/cohort-topic/traffic.js.map +1 -1
  90. package/dist/src/cohort-topic/walk.d.ts +22 -7
  91. package/dist/src/cohort-topic/walk.d.ts.map +1 -1
  92. package/dist/src/cohort-topic/walk.js +78 -18
  93. package/dist/src/cohort-topic/walk.js.map +1 -1
  94. package/dist/src/cohort-topic/wire/codec.d.ts +3 -1
  95. package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -1
  96. package/dist/src/cohort-topic/wire/codec.js +7 -1
  97. package/dist/src/cohort-topic/wire/codec.js.map +1 -1
  98. package/dist/src/cohort-topic/wire/index.d.ts +1 -1
  99. package/dist/src/cohort-topic/wire/index.d.ts.map +1 -1
  100. package/dist/src/cohort-topic/wire/index.js +1 -1
  101. package/dist/src/cohort-topic/wire/index.js.map +1 -1
  102. package/dist/src/cohort-topic/wire/payloads.d.ts +5 -4
  103. package/dist/src/cohort-topic/wire/payloads.d.ts.map +1 -1
  104. package/dist/src/cohort-topic/wire/payloads.js +14 -5
  105. package/dist/src/cohort-topic/wire/payloads.js.map +1 -1
  106. package/dist/src/cohort-topic/wire/primitives.d.ts +65 -0
  107. package/dist/src/cohort-topic/wire/primitives.d.ts.map +1 -0
  108. package/dist/src/cohort-topic/wire/primitives.js +173 -0
  109. package/dist/src/cohort-topic/wire/primitives.js.map +1 -0
  110. package/dist/src/cohort-topic/wire/types.d.ts +132 -5
  111. package/dist/src/cohort-topic/wire/types.d.ts.map +1 -1
  112. package/dist/src/cohort-topic/wire/validate.d.ts +15 -5
  113. package/dist/src/cohort-topic/wire/validate.d.ts.map +1 -1
  114. package/dist/src/cohort-topic/wire/validate.js +146 -123
  115. package/dist/src/cohort-topic/wire/validate.js.map +1 -1
  116. package/dist/src/collection/collection.d.ts +23 -12
  117. package/dist/src/collection/collection.d.ts.map +1 -1
  118. package/dist/src/collection/collection.js +111 -35
  119. package/dist/src/collection/collection.js.map +1 -1
  120. package/dist/src/collection/struct.d.ts +37 -2
  121. package/dist/src/collection/struct.d.ts.map +1 -1
  122. package/dist/src/collection/struct.js +16 -1
  123. package/dist/src/collection/struct.js.map +1 -1
  124. package/dist/src/collections/tree/tree.d.ts +18 -1
  125. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  126. package/dist/src/collections/tree/tree.js +36 -6
  127. package/dist/src/collections/tree/tree.js.map +1 -1
  128. package/dist/src/index.d.ts +1 -0
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +1 -0
  131. package/dist/src/index.js.map +1 -1
  132. package/dist/src/log/log.d.ts +26 -3
  133. package/dist/src/log/log.d.ts.map +1 -1
  134. package/dist/src/log/log.js +34 -7
  135. package/dist/src/log/log.js.map +1 -1
  136. package/dist/src/log/struct.d.ts +5 -4
  137. package/dist/src/log/struct.d.ts.map +1 -1
  138. package/dist/src/log/struct.js.map +1 -1
  139. package/dist/src/matchmaking/wire.d.ts +2 -1
  140. package/dist/src/matchmaking/wire.d.ts.map +1 -1
  141. package/dist/src/matchmaking/wire.js +13 -92
  142. package/dist/src/matchmaking/wire.js.map +1 -1
  143. package/dist/src/network/i-key-network.d.ts +9 -0
  144. package/dist/src/network/i-key-network.d.ts.map +1 -1
  145. package/dist/src/network/i-repo.d.ts +8 -0
  146. package/dist/src/network/i-repo.d.ts.map +1 -1
  147. package/dist/src/network/struct.d.ts +10 -0
  148. package/dist/src/network/struct.d.ts.map +1 -1
  149. package/dist/src/reactivity/push-state.d.ts.map +1 -1
  150. package/dist/src/reactivity/push-state.js +4 -33
  151. package/dist/src/reactivity/push-state.js.map +1 -1
  152. package/dist/src/reactivity/wire-validate.d.ts +7 -22
  153. package/dist/src/reactivity/wire-validate.d.ts.map +1 -1
  154. package/dist/src/reactivity/wire-validate.js +7 -73
  155. package/dist/src/reactivity/wire-validate.js.map +1 -1
  156. package/dist/src/reactivity/wire.js +3 -3
  157. package/dist/src/reactivity/wire.js.map +1 -1
  158. package/dist/src/testing/async-wait.d.ts +23 -0
  159. package/dist/src/testing/async-wait.d.ts.map +1 -0
  160. package/dist/src/testing/async-wait.js +42 -0
  161. package/dist/src/testing/async-wait.js.map +1 -0
  162. package/dist/src/testing/index.d.ts +3 -0
  163. package/dist/src/testing/index.d.ts.map +1 -0
  164. package/dist/src/testing/index.js +3 -0
  165. package/dist/src/testing/index.js.map +1 -0
  166. package/dist/src/testing/test-transactor.d.ts +44 -0
  167. package/dist/src/testing/test-transactor.d.ts.map +1 -0
  168. package/dist/src/testing/test-transactor.js +423 -0
  169. package/dist/src/testing/test-transactor.js.map +1 -0
  170. package/dist/src/transaction/actions-engine.d.ts +6 -3
  171. package/dist/src/transaction/actions-engine.d.ts.map +1 -1
  172. package/dist/src/transaction/actions-engine.js +8 -7
  173. package/dist/src/transaction/actions-engine.js.map +1 -1
  174. package/dist/src/transaction/coordinator.d.ts +43 -22
  175. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  176. package/dist/src/transaction/coordinator.js +443 -178
  177. package/dist/src/transaction/coordinator.js.map +1 -1
  178. package/dist/src/transaction/errors.d.ts +82 -0
  179. package/dist/src/transaction/errors.d.ts.map +1 -0
  180. package/dist/src/transaction/errors.js +93 -0
  181. package/dist/src/transaction/errors.js.map +1 -0
  182. package/dist/src/transaction/index.d.ts +6 -3
  183. package/dist/src/transaction/index.d.ts.map +1 -1
  184. package/dist/src/transaction/index.js +4 -2
  185. package/dist/src/transaction/index.js.map +1 -1
  186. package/dist/src/transaction/operations-hash.d.ts +119 -0
  187. package/dist/src/transaction/operations-hash.d.ts.map +1 -0
  188. package/dist/src/transaction/operations-hash.js +180 -0
  189. package/dist/src/transaction/operations-hash.js.map +1 -0
  190. package/dist/src/transaction/read-dependency-collector.d.ts +43 -0
  191. package/dist/src/transaction/read-dependency-collector.d.ts.map +1 -0
  192. package/dist/src/transaction/read-dependency-collector.js +69 -0
  193. package/dist/src/transaction/read-dependency-collector.js.map +1 -0
  194. package/dist/src/transaction/session.d.ts +12 -3
  195. package/dist/src/transaction/session.d.ts.map +1 -1
  196. package/dist/src/transaction/session.js +35 -9
  197. package/dist/src/transaction/session.js.map +1 -1
  198. package/dist/src/transaction/transaction.d.ts +162 -1
  199. package/dist/src/transaction/transaction.d.ts.map +1 -1
  200. package/dist/src/transaction/transaction.js +85 -3
  201. package/dist/src/transaction/transaction.js.map +1 -1
  202. package/dist/src/transaction/validator.d.ts +3 -11
  203. package/dist/src/transaction/validator.d.ts.map +1 -1
  204. package/dist/src/transaction/validator.js +69 -28
  205. package/dist/src/transaction/validator.js.map +1 -1
  206. package/dist/src/transactor/network-transactor.d.ts +35 -0
  207. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  208. package/dist/src/transactor/network-transactor.js +146 -40
  209. package/dist/src/transactor/network-transactor.js.map +1 -1
  210. package/dist/src/transactor/transactor-source.d.ts +19 -5
  211. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  212. package/dist/src/transactor/transactor-source.js +43 -11
  213. package/dist/src/transactor/transactor-source.js.map +1 -1
  214. package/dist/src/transform/atomic-proxy.d.ts +35 -5
  215. package/dist/src/transform/atomic-proxy.d.ts.map +1 -1
  216. package/dist/src/transform/atomic-proxy.js +39 -7
  217. package/dist/src/transform/atomic-proxy.js.map +1 -1
  218. package/dist/src/transform/cache-source.d.ts +36 -5
  219. package/dist/src/transform/cache-source.d.ts.map +1 -1
  220. package/dist/src/transform/cache-source.js +84 -8
  221. package/dist/src/transform/cache-source.js.map +1 -1
  222. package/dist/src/transform/helpers.d.ts.map +1 -1
  223. package/dist/src/transform/helpers.js +8 -2
  224. package/dist/src/transform/helpers.js.map +1 -1
  225. package/dist/src/transform/tracker.d.ts +17 -2
  226. package/dist/src/transform/tracker.d.ts.map +1 -1
  227. package/dist/src/transform/tracker.js +66 -11
  228. package/dist/src/transform/tracker.js.map +1 -1
  229. package/dist/src/utility/backoff.d.ts +48 -0
  230. package/dist/src/utility/backoff.d.ts.map +1 -0
  231. package/dist/src/utility/backoff.js +60 -0
  232. package/dist/src/utility/backoff.js.map +1 -0
  233. package/dist/src/utility/pending.d.ts +1 -0
  234. package/dist/src/utility/pending.d.ts.map +1 -1
  235. package/dist/src/utility/pending.js +3 -1
  236. package/dist/src/utility/pending.js.map +1 -1
  237. package/package.json +4 -4
  238. package/src/blocks/block-store.ts +19 -1
  239. package/src/blocks/helpers.ts +3 -3
  240. package/src/btree/btree.ts +108 -63
  241. package/src/chain/chain.ts +11 -1
  242. package/src/cluster/index.ts +1 -0
  243. package/src/cluster/membership.ts +85 -0
  244. package/src/cluster/structs.ts +51 -1
  245. package/src/cohort-topic/addressing.ts +12 -4
  246. package/src/cohort-topic/antidos/bootstrap-evidence.ts +21 -9
  247. package/src/cohort-topic/antidos/replay-guard.ts +37 -0
  248. package/src/cohort-topic/antidos/topic-budget.ts +17 -2
  249. package/src/cohort-topic/coldstart.ts +48 -15
  250. package/src/cohort-topic/gossip/bus.ts +39 -2
  251. package/src/cohort-topic/gossip/view.ts +26 -1
  252. package/src/cohort-topic/member-engine.ts +36 -15
  253. package/src/cohort-topic/membership/publisher.ts +25 -33
  254. package/src/cohort-topic/membership/verifier.ts +119 -3
  255. package/src/cohort-topic/promotion.ts +23 -2
  256. package/src/cohort-topic/registration/renewal.ts +73 -1
  257. package/src/cohort-topic/registration/types.ts +15 -0
  258. package/src/cohort-topic/service.ts +41 -15
  259. package/src/cohort-topic/sig/payloads.ts +29 -9
  260. package/src/cohort-topic/traffic.ts +23 -1
  261. package/src/cohort-topic/walk.ts +95 -24
  262. package/src/cohort-topic/wire/codec.ts +12 -0
  263. package/src/cohort-topic/wire/index.ts +2 -0
  264. package/src/cohort-topic/wire/payloads.ts +14 -5
  265. package/src/cohort-topic/wire/primitives.ts +188 -0
  266. package/src/cohort-topic/wire/types.ts +136 -4
  267. package/src/cohort-topic/wire/validate.ts +168 -134
  268. package/src/collection/collection.ts +123 -40
  269. package/src/collection/struct.ts +44 -2
  270. package/src/collections/tree/tree.ts +42 -5
  271. package/src/index.ts +1 -0
  272. package/src/log/log.ts +37 -9
  273. package/src/log/struct.ts +5 -4
  274. package/src/matchmaking/wire.ts +25 -102
  275. package/src/network/i-key-network.ts +10 -0
  276. package/src/network/i-repo.ts +8 -0
  277. package/src/network/struct.ts +10 -0
  278. package/src/reactivity/push-state.ts +4 -37
  279. package/src/reactivity/wire-validate.ts +7 -81
  280. package/src/reactivity/wire.ts +3 -3
  281. package/src/testing/async-wait.ts +65 -0
  282. package/src/testing/index.ts +2 -0
  283. package/src/testing/test-transactor.ts +468 -0
  284. package/src/transaction/actions-engine.ts +8 -6
  285. package/src/transaction/coordinator.ts +498 -229
  286. package/src/transaction/errors.ts +91 -0
  287. package/src/transaction/index.ts +23 -3
  288. package/src/transaction/operations-hash.ts +196 -0
  289. package/src/transaction/read-dependency-collector.ts +78 -0
  290. package/src/transaction/session.ts +37 -9
  291. package/src/transaction/transaction.ts +211 -4
  292. package/src/transaction/validator.ts +73 -48
  293. package/src/transactor/network-transactor.ts +164 -49
  294. package/src/transactor/transactor-source.ts +47 -12
  295. package/src/transform/atomic-proxy.ts +92 -49
  296. package/src/transform/cache-source.ts +87 -9
  297. package/src/transform/helpers.ts +8 -2
  298. package/src/transform/tracker.ts +69 -11
  299. package/src/utility/backoff.ts +95 -0
  300. package/src/utility/pending.ts +3 -1
  301. 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
- const stampData = JSON.stringify({ peerId, timestamp, schemaHash, engineId, expiration });
94
- const id = `stamp:${await hashString(stampData)}`;
95
- return { peerId, timestamp, schemaHash, engineId, expiration, id };
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, IBlock, BlockOperations, Transforms } from '../index.js';
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 { hashString } from '../utility/hash-string.js';
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. Check expiration before any other work
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
- const computedHash = await this.hashOperations(allOperations);
125
-
126
- // 9. Compare with sender's hash
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