@bsv/overlay 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -106
- package/dist/cjs/mod.js +6 -1
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +21 -11
- package/dist/cjs/src/BASM.js +92 -0
- package/dist/cjs/src/BASM.js.map +1 -0
- package/dist/cjs/src/BASMRemote.js +43 -0
- package/dist/cjs/src/BASMRemote.js.map +1 -0
- package/dist/cjs/src/Engine.js +649 -47
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPRemote.js +2 -2
- package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.js +18 -25
- package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/KnexStorage.js +329 -12
- package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/all-migrations.js +3 -1
- package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js +2 -24
- package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js +114 -0
- package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +1 -0
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/BASM.js +85 -0
- package/dist/esm/src/BASM.js.map +1 -0
- package/dist/esm/src/BASMRemote.js +42 -0
- package/dist/esm/src/BASMRemote.js.map +1 -0
- package/dist/esm/src/Engine.js +637 -46
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPRemote.js +2 -2
- package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPStorage.js +18 -25
- package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/KnexStorage.js +323 -12
- package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/all-migrations.js +3 -1
- package/dist/esm/src/storage/knex/all-migrations.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2025-05-28-001-enlarge.js +2 -24
- package/dist/esm/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js +109 -0
- package/dist/esm/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +2 -0
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/BASM.d.ts +110 -0
- package/dist/types/src/BASM.d.ts.map +1 -0
- package/dist/types/src/BASMRemote.d.ts +14 -0
- package/dist/types/src/BASMRemote.d.ts.map +1 -0
- package/dist/types/src/Engine.d.ts +78 -1
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
- package/dist/types/src/LookupService.d.ts.map +1 -1
- package/dist/types/src/storage/Storage.d.ts +118 -0
- package/dist/types/src/storage/Storage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/KnexStorage.d.ts +38 -6
- package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2025-05-28-001-enlarge.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts +4 -0
- package/dist/types/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts.map +1 -0
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/docs/API.md +72 -580
- package/docs/BRC-136-BASM.md +285 -0
- package/docs/README.md +2 -0
- package/docs/Synchronization.md +10 -1
- package/docs/examples/README.md +2 -2
- package/docs/examples/gs-wip.md +68 -76
- package/mod.ts +25 -1
- package/package.json +33 -23
- package/src/BASM.ts +208 -0
- package/src/BASMRemote.ts +54 -0
- package/src/Engine.ts +762 -47
- package/src/GASP/OverlayGASPRemote.ts +2 -2
- package/src/GASP/OverlayGASPStorage.ts +17 -22
- package/src/LookupService.ts +2 -1
- package/src/__tests/BASM.test.ts +44 -0
- package/src/__tests/BASMChain.test.ts +290 -0
- package/src/__tests/Engine.test.ts +3 -4
- package/src/storage/Storage.ts +127 -0
- package/src/storage/knex/KnexStorage.ts +376 -14
- package/src/storage/knex/all-migrations.ts +3 -1
- package/src/storage/knex/migrations/2025-05-28-001-enlarge.ts +2 -22
- 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
|
+
}
|