@bsv/overlay 2.0.3 → 2.0.4

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 (86) hide show
  1. package/README.md +226 -106
  2. package/dist/cjs/mod.js +6 -1
  3. package/dist/cjs/mod.js.map +1 -1
  4. package/dist/cjs/package.json +13 -11
  5. package/dist/cjs/src/BASM.js +92 -0
  6. package/dist/cjs/src/BASM.js.map +1 -0
  7. package/dist/cjs/src/BASMRemote.js +43 -0
  8. package/dist/cjs/src/BASMRemote.js.map +1 -0
  9. package/dist/cjs/src/Engine.js +649 -47
  10. package/dist/cjs/src/Engine.js.map +1 -1
  11. package/dist/cjs/src/GASP/OverlayGASPRemote.js +2 -2
  12. package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
  13. package/dist/cjs/src/GASP/OverlayGASPStorage.js +18 -25
  14. package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
  15. package/dist/cjs/src/storage/knex/KnexStorage.js +329 -12
  16. package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
  17. package/dist/cjs/src/storage/knex/all-migrations.js +3 -1
  18. package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
  19. package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js +2 -24
  20. package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
  21. package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js +114 -0
  22. package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -0
  23. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  24. package/dist/esm/mod.js +1 -0
  25. package/dist/esm/mod.js.map +1 -1
  26. package/dist/esm/src/BASM.js +85 -0
  27. package/dist/esm/src/BASM.js.map +1 -0
  28. package/dist/esm/src/BASMRemote.js +42 -0
  29. package/dist/esm/src/BASMRemote.js.map +1 -0
  30. package/dist/esm/src/Engine.js +637 -46
  31. package/dist/esm/src/Engine.js.map +1 -1
  32. package/dist/esm/src/GASP/OverlayGASPRemote.js +2 -2
  33. package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
  34. package/dist/esm/src/GASP/OverlayGASPStorage.js +18 -25
  35. package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
  36. package/dist/esm/src/storage/knex/KnexStorage.js +323 -12
  37. package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
  38. package/dist/esm/src/storage/knex/all-migrations.js +3 -1
  39. package/dist/esm/src/storage/knex/all-migrations.js.map +1 -1
  40. package/dist/esm/src/storage/knex/migrations/2025-05-28-001-enlarge.js +2 -24
  41. package/dist/esm/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
  42. package/dist/esm/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js +109 -0
  43. package/dist/esm/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -0
  44. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  45. package/dist/types/mod.d.ts +2 -0
  46. package/dist/types/mod.d.ts.map +1 -1
  47. package/dist/types/src/BASM.d.ts +110 -0
  48. package/dist/types/src/BASM.d.ts.map +1 -0
  49. package/dist/types/src/BASMRemote.d.ts +14 -0
  50. package/dist/types/src/BASMRemote.d.ts.map +1 -0
  51. package/dist/types/src/Engine.d.ts +78 -1
  52. package/dist/types/src/Engine.d.ts.map +1 -1
  53. package/dist/types/src/GASP/OverlayGASPStorage.d.ts +1 -1
  54. package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
  55. package/dist/types/src/LookupService.d.ts.map +1 -1
  56. package/dist/types/src/storage/Storage.d.ts +118 -0
  57. package/dist/types/src/storage/Storage.d.ts.map +1 -1
  58. package/dist/types/src/storage/knex/KnexStorage.d.ts +38 -6
  59. package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
  60. package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
  61. package/dist/types/src/storage/knex/migrations/2025-05-28-001-enlarge.d.ts.map +1 -1
  62. package/dist/types/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts +4 -0
  63. package/dist/types/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts.map +1 -0
  64. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  65. package/docs/API.md +72 -580
  66. package/docs/BRC-136-BASM.md +285 -0
  67. package/docs/README.md +2 -0
  68. package/docs/Synchronization.md +10 -1
  69. package/docs/examples/README.md +2 -2
  70. package/docs/examples/gs-wip.md +68 -76
  71. package/mod.ts +25 -1
  72. package/package.json +25 -23
  73. package/src/BASM.ts +208 -0
  74. package/src/BASMRemote.ts +54 -0
  75. package/src/Engine.ts +762 -47
  76. package/src/GASP/OverlayGASPRemote.ts +2 -2
  77. package/src/GASP/OverlayGASPStorage.ts +17 -22
  78. package/src/LookupService.ts +2 -1
  79. package/src/__tests/BASM.test.ts +44 -0
  80. package/src/__tests/BASMChain.test.ts +290 -0
  81. package/src/__tests/Engine.test.ts +3 -4
  82. package/src/storage/Storage.ts +127 -0
  83. package/src/storage/knex/KnexStorage.ts +376 -14
  84. package/src/storage/knex/all-migrations.ts +3 -1
  85. package/src/storage/knex/migrations/2025-05-28-001-enlarge.ts +2 -22
  86. package/src/storage/knex/migrations/2026-05-29-001-brc136-basm.ts +123 -0
package/src/BASM.ts ADDED
@@ -0,0 +1,208 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { MerklePath } from '@bsv/sdk'
3
+
4
+ export const BASM_ZERO_HASH = '0000000000000000000000000000000000000000000000000000000000000000'
5
+
6
+ export interface TopicBlockAnchor {
7
+ topic: string
8
+ blockHeight: number
9
+ blockHash: string
10
+ basmRoot: string
11
+ admittedCount: number
12
+ tac: string
13
+ }
14
+
15
+ export interface TopicAnchorTip {
16
+ topic: string
17
+ blockHeight: number
18
+ blockHash?: string
19
+ basmRoot?: string
20
+ admittedCount?: number
21
+ tac: string
22
+ }
23
+
24
+ export interface AdmittedTxRef {
25
+ txid: string
26
+ blockIndex: number
27
+ }
28
+
29
+ export interface RawTransactionRecord {
30
+ txid: string
31
+ rawTx: string
32
+ }
33
+
34
+ export interface RawTransactionRequest {
35
+ txids: string[]
36
+ }
37
+
38
+ export interface RawTransactionResponse {
39
+ transactions: RawTransactionRecord[]
40
+ missing: string[]
41
+ }
42
+
43
+ export interface TopicAnchorHeader {
44
+ blockHeight: number
45
+ blockHash: string
46
+ merkleRoot?: string
47
+ }
48
+
49
+ export type TopicAnchorHeaderResolver = (blockHeight: number) => Promise<TopicAnchorHeader | undefined>
50
+
51
+ export interface TopicAnchorRangeRequest {
52
+ fromHeight: number
53
+ toHeight: number
54
+ }
55
+
56
+ export interface TopicAnchorRangeResponse {
57
+ topic: string
58
+ anchors: TopicBlockAnchor[]
59
+ }
60
+
61
+ export interface AdmittedListRequest {
62
+ blockHeight: number
63
+ blockHash?: string
64
+ }
65
+
66
+ export interface AdmittedListResponse {
67
+ topic: string
68
+ blockHeight: number
69
+ blockHash?: string
70
+ admitted: AdmittedTxRef[]
71
+ }
72
+
73
+ export interface CompoundMerklePathRequest {
74
+ blockHeight: number
75
+ txids: string[]
76
+ }
77
+
78
+ export interface CompoundMerklePathResponse {
79
+ topic: string
80
+ blockHeight: number
81
+ txids: string[]
82
+ merklePath: string
83
+ }
84
+
85
+ export interface MerkleProofMetadata {
86
+ blockHeight: number
87
+ blockIndex: number
88
+ merkleRoot: string
89
+ }
90
+
91
+ export interface ReorgReport {
92
+ perTopic: Array<{
93
+ topic: string
94
+ demotedTxids: string[]
95
+ rebuiltFrom: number
96
+ rebuiltTo: number
97
+ }>
98
+ }
99
+
100
+ export interface BASMPeerSyncReport {
101
+ topic: string
102
+ endpoint: string
103
+ status: 'matched' | 'advanced' | 'diverged' | 'skipped' | 'error'
104
+ localTip?: TopicAnchorTip
105
+ remoteTip?: TopicAnchorTip
106
+ checkedHeights: number[]
107
+ missingTxids: string[]
108
+ fetchedTxCount: number
109
+ message?: string
110
+ }
111
+
112
+ type AdmittedTxLike = string | AdmittedTxRef
113
+
114
+ function sha256d(buffer: Buffer): Buffer {
115
+ const first = createHash('sha256').update(buffer).digest()
116
+ return createHash('sha256').update(first).digest()
117
+ }
118
+
119
+ function assertHashHex(hash: string, label: string): void {
120
+ if (!/^[0-9a-fA-F]{64}$/.test(hash)) {
121
+ throw new Error(`${label} must be 32 bytes of hex`)
122
+ }
123
+ }
124
+
125
+ function displayHexToInternal(hash: string): Buffer {
126
+ assertHashHex(hash, 'hash')
127
+ return Buffer.from(hash, 'hex').reverse()
128
+ }
129
+
130
+ function internalToDisplayHex(hash: Buffer): string {
131
+ return Buffer.from(hash).reverse().toString('hex')
132
+ }
133
+
134
+ function normalizeAdmittedTxids(admitted: AdmittedTxLike[]): string[] {
135
+ return admitted
136
+ .map((item, originalIndex) => {
137
+ if (typeof item === 'string') {
138
+ return { txid: item, blockIndex: originalIndex }
139
+ }
140
+ return item
141
+ })
142
+ .sort((a, b) => a.blockIndex - b.blockIndex)
143
+ .map(item => item.txid.toLowerCase())
144
+ }
145
+
146
+ /**
147
+ * Computes a BRC-136 BASM root from admitted topic txids.
148
+ *
149
+ * The API accepts normal display-order txid hex because that is what the BSV
150
+ * TypeScript stack exposes at its public boundaries. Hashing is performed on
151
+ * internal byte order as required by the BRC, and the returned root is display
152
+ * order for JSON/wire compatibility.
153
+ */
154
+ export function computeBasmRoot(admitted: AdmittedTxLike[]): string {
155
+ const txids = normalizeAdmittedTxids(admitted)
156
+
157
+ if (txids.length === 0) {
158
+ return BASM_ZERO_HASH
159
+ }
160
+
161
+ let layer = txids.map(txid => displayHexToInternal(txid))
162
+ if (layer.length === 1) {
163
+ return internalToDisplayHex(layer[0])
164
+ }
165
+
166
+ while (layer.length > 1) {
167
+ const next: Buffer[] = []
168
+ for (let i = 0; i < layer.length; i += 2) {
169
+ const left = layer[i]
170
+ const right = i + 1 < layer.length ? layer[i + 1] : left
171
+ next.push(sha256d(Buffer.concat([left, right])))
172
+ }
173
+ layer = next
174
+ }
175
+
176
+ return internalToDisplayHex(layer[0])
177
+ }
178
+
179
+ /**
180
+ * Computes the BRC-136 cumulative Topic Anchor Chain hash:
181
+ * SHA256d(prevTac || blockHash || basmRoot), with all inputs reversed to
182
+ * internal byte order before hashing and the output returned as display hex.
183
+ */
184
+ export function computeTac(prevTac: string, blockHash: string, basmRoot: string): string {
185
+ const input = Buffer.concat([
186
+ displayHexToInternal(prevTac.toLowerCase()),
187
+ displayHexToInternal(blockHash.toLowerCase()),
188
+ displayHexToInternal(basmRoot.toLowerCase())
189
+ ])
190
+ return internalToDisplayHex(sha256d(input))
191
+ }
192
+
193
+ export function extractMerkleProofMetadata(txid: string, proof?: MerklePath): MerkleProofMetadata | undefined {
194
+ if (proof === undefined) {
195
+ return undefined
196
+ }
197
+
198
+ const leaf = proof.path[0]?.find(candidate => candidate.hash === txid)
199
+ if (leaf === undefined) {
200
+ return undefined
201
+ }
202
+
203
+ return {
204
+ blockHeight: proof.blockHeight,
205
+ blockIndex: leaf.offset,
206
+ merkleRoot: proof.computeRoot(txid)
207
+ }
208
+ }
@@ -0,0 +1,54 @@
1
+ import type {
2
+ AdmittedListResponse,
3
+ CompoundMerklePathResponse,
4
+ RawTransactionResponse,
5
+ TopicAnchorRangeResponse,
6
+ TopicAnchorTip
7
+ } from './BASM.js'
8
+
9
+ export class BASMRemote {
10
+ constructor(
11
+ private readonly endpoint: string,
12
+ private readonly topic: string,
13
+ private readonly fetchImpl: typeof fetch = fetch.bind(globalThis)
14
+ ) { }
15
+
16
+ async requestTopicAnchorTip(): Promise<TopicAnchorTip> {
17
+ return await this.post<TopicAnchorTip>('/requestTopicAnchorTip', {})
18
+ }
19
+
20
+ async requestTopicAnchorRange(fromHeight: number, toHeight: number): Promise<TopicAnchorRangeResponse> {
21
+ return await this.post<TopicAnchorRangeResponse>('/requestTopicAnchorRange', { fromHeight, toHeight })
22
+ }
23
+
24
+ async requestAdmittedList(blockHeight: number, blockHash?: string): Promise<AdmittedListResponse> {
25
+ return await this.post<AdmittedListResponse>('/requestAdmittedList', { blockHeight, blockHash })
26
+ }
27
+
28
+ async requestCompoundMerklePath(blockHeight: number, txids: string[]): Promise<CompoundMerklePathResponse> {
29
+ return await this.post<CompoundMerklePathResponse>('/requestCompoundMerklePath', { blockHeight, txids })
30
+ }
31
+
32
+ async requestRawTransactions(txids: string[]): Promise<RawTransactionResponse> {
33
+ return await this.post<RawTransactionResponse>('/requestRawTransactions', { txids })
34
+ }
35
+
36
+ private async post<T>(path: string, body: unknown): Promise<T> {
37
+ const response = await this.fetchImpl(new URL(path, this.endpoint).toString(), {
38
+ method: 'POST',
39
+ headers: {
40
+ Accept: 'application/json',
41
+ 'Content-Type': 'application/json',
42
+ 'x-bsv-topic': this.topic
43
+ },
44
+ body: JSON.stringify(body)
45
+ })
46
+
47
+ const text = await response.text()
48
+ if (!response.ok) {
49
+ throw new Error(`BASM peer ${this.endpoint} returned ${response.status}: ${text}`)
50
+ }
51
+
52
+ return (text.length === 0 ? {} : JSON.parse(text)) as T
53
+ }
54
+ }