Skip to content

Commit 412d8cb

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/bdk
# Conflicts: # pnpm-lock.yaml
2 parents 420701c + 0c7685f commit 412d8cb

141 files changed

Lines changed: 5669 additions & 1391 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
with:
2323
fetch-depth: 0
2424

25-
- uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6
25+
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
2626

2727
- name: Use Node.js ${{ matrix.node-version }}
2828
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
@@ -62,13 +62,14 @@ jobs:
6262
fi
6363
6464
- name: Build changed packages
65+
# Build the FULL workspace (not just the changed scope): a changed
66+
# package's workspace dependencies must have their built dist present
67+
# for tsc to resolve them, but those dependencies are not part of the
68+
# `...[BASE]` changed+dependents scope. This matches the push-event
69+
# build (which already builds everything). Lint/test stay scoped below.
6570
# Exclude: workspace root (delegating script) and example-paymail (example package, not production)
66-
env:
67-
SCOPE_FILTER: ${{ steps.scope.outputs.filter }}
6871
run: |
69-
EXTRA=""
70-
if [ -n "$SCOPE_FILTER" ]; then EXTRA="--filter $SCOPE_FILTER"; fi
71-
pnpm -r --filter '!@bsv/ts-stack' --filter '!example-paymail' $EXTRA run build
72+
pnpm -r --filter '!@bsv/ts-stack' --filter '!example-paymail' run build
7273
7374
- name: Lint changed packages
7475
continue-on-error: true
@@ -79,6 +80,17 @@ jobs:
7980
if [ -n "$SCOPE_FILTER" ]; then EXTRA="--filter $SCOPE_FILTER"; fi
8081
pnpm -r --filter '!@bsv/ts-stack' $EXTRA run lint --if-present
8182
83+
- name: Warm MongoDB memory-server binary cache
84+
# The mongodb-memory-server tests download+extract the mongod binary on first
85+
# use. When that cold start happens under the parallel `pnpm -r run test` load
86+
# below, the just-extracted binary can still be open for writing when it is
87+
# exec'd, producing `spawn ETXTBSY` and startup timeouts (seen on Node 24).
88+
# Pre-extract the binary serially here so the test step reuses a settled cache.
89+
continue-on-error: true
90+
run: >
91+
pnpm --filter @bsv/overlay-topics exec node --input-type=module -e
92+
"import { MongoMemoryServer } from 'mongodb-memory-server'; const s = await MongoMemoryServer.create({ instance: { launchTimeout: 120000 } }); await s.stop(); console.log('mongodb-memory-server binary cache warmed');"
93+
8294
- name: Test changed packages
8395
# Exclude: workspace root (delegating script) and example-paymail (example package, not production)
8496
env:
@@ -142,7 +154,7 @@ jobs:
142154
with:
143155
fetch-depth: 1
144156

145-
- uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6
157+
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
146158

147159
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
148160
with:
@@ -167,7 +179,7 @@ jobs:
167179
steps:
168180
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
169181

170-
- uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6
182+
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
171183
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
172184
with:
173185
node-version: 24

.github/workflows/codegen.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
# NOTE: oapi-codegen does not yet support OpenAPI 3.1 nullable syntax
1919
# (type: [string, null]). Steps are continue-on-error until resolved.
2020
# Upstream: https://github.com/oapi-codegen/oapi-codegen/issues/373
21-
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
21+
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
2222
with:
2323
go-version: '1.22'
2424
- name: Install oapi-codegen
@@ -117,7 +117,7 @@ jobs:
117117
EOF
118118
119119
# ── Single commit for all generated files ────────────────────────────
120-
- uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5
120+
- uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
121121
with:
122122
commit_message: "chore(codegen): regenerate types from OpenAPI specs"
123123
file_pattern: "conformance/generated/**"

.github/workflows/conformance.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
1515
with:
1616
node-version: '24'
17-
- uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6
17+
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
1818
- name: Install deps
1919
run: pnpm install --no-frozen-lockfile
2020
- name: Build TS SDK

.github/workflows/docs-deploy.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
with:
2626
fetch-depth: 0
2727

28-
- uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6
28+
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
2929

3030
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
3131
with:
@@ -49,9 +49,9 @@ jobs:
4949
env:
5050
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5151

52-
- uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
52+
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
5353

54-
- uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
54+
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
5555
with:
5656
path: docs-site/dist
5757

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
with:
3232
fetch-depth: 0
3333

34-
- uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6
34+
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
3535

3636
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
3737
with:

.sonarcloud.properties

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# SonarCloud Automatic Analysis configuration.
2+
#
3+
# Automatic Analysis reads THIS file (.sonarcloud.properties), not
4+
# sonar-project.properties. We exclude test files from copy-paste (duplication)
5+
# detection: parallel test suites (e.g. SQLite vs IndexedDB variants of the same
6+
# storage tests) legitimately duplicate large blocks, and tests are already
7+
# excluded from sources/coverage analysis.
8+
sonar.cpd.exclusions=**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/*.man.test.ts,**/__test__/**,**/__tests__/**,**/test/**,**/tests/**,**/*.vectors.ts,**/eslint.config.js

conformance/runner/ts/dispatchers/sync.ts

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@
3535
*/
3636

3737
import { expect } from '@jest/globals'
38+
import { createHash } from 'node:crypto'
3839

3940
export const categories: ReadonlyArray<string> = [
4041
'gasp-protocol',
4142
'brc40-user-state',
42-
'chaintracks-v2-http'
43+
'chaintracks-v2-http',
44+
'brc136-basm'
4345
]
4446

4547
// ── Constants ──────────────────────────────────────────────────────────────────
@@ -49,6 +51,7 @@ const GASP_CURRENT_VERSION = 1
4951

5052
/** txid pattern: exactly 64 hex characters (upper or lower). */
5153
const TXID_RE = /^[0-9a-fA-F]{64}$/
54+
const ZERO_HASH = '0000000000000000000000000000000000000000000000000000000000000000'
5255

5356
// ── Helpers ────────────────────────────────────────────────────────────────────
5457

@@ -787,6 +790,123 @@ function dispatchChaintracksV2 (
787790
}
788791
}
789792

793+
// ── BRC-136 BASM ──────────────────────────────────────────────────────────────
794+
795+
function sha256d(buffer: Buffer): Buffer {
796+
const first = createHash('sha256').update(buffer).digest()
797+
return createHash('sha256').update(first).digest()
798+
}
799+
800+
function displayToInternal(hash: string): Buffer {
801+
expect(TXID_RE.test(hash)).toBe(true)
802+
return Buffer.from(hash, 'hex').reverse()
803+
}
804+
805+
function internalToDisplay(hash: Buffer): string {
806+
return Buffer.from(hash).reverse().toString('hex')
807+
}
808+
809+
function computeBasmRootForVector(txids: string[]): string {
810+
if (txids.length === 0) return ZERO_HASH
811+
let layer = txids.map(displayToInternal)
812+
if (layer.length === 1) return internalToDisplay(layer[0])
813+
while (layer.length > 1) {
814+
const next: Buffer[] = []
815+
for (let i = 0; i < layer.length; i += 2) {
816+
const right = i + 1 < layer.length ? layer[i + 1] : layer[i]
817+
next.push(sha256d(Buffer.concat([layer[i], right])))
818+
}
819+
layer = next
820+
}
821+
return internalToDisplay(layer[0])
822+
}
823+
824+
function computeTacForVector(prevTac: string, blockHash: string, basmRoot: string): string {
825+
return internalToDisplay(sha256d(Buffer.concat([
826+
displayToInternal(prevTac),
827+
displayToInternal(blockHash),
828+
displayToInternal(basmRoot)
829+
])))
830+
}
831+
832+
function dispatchBRC136HTTP(input: Record<string, unknown>, expected: Record<string, unknown>): void {
833+
const method = getString(input, 'method')
834+
const path = getString(input, 'path')
835+
const headers = (input['headers'] ?? {}) as Record<string, string>
836+
const expectedStatus = getNumber(expected, 'status')
837+
838+
expect(method).toBe('POST')
839+
expect(path.startsWith('/request')).toBe(true)
840+
const requiresTopic = path !== '/requestRawTransactions'
841+
if (requiresTopic) {
842+
const hasTopic = Object.keys(headers).some(k => k.toLowerCase() === 'x-bsv-topic')
843+
expect(hasTopic).toBe(expectedStatus < 400)
844+
}
845+
846+
if (expectedStatus >= 400) {
847+
const body = expected['body']
848+
expect(body !== undefined).toBe(true)
849+
assertErrorEnvelope(body as Record<string, unknown>)
850+
return
851+
}
852+
853+
if (expected['body'] !== undefined) {
854+
const body = expected['body'] as Record<string, unknown>
855+
if (path === '/requestTopicAnchorTip') {
856+
expect(typeof body['topic']).toBe('string')
857+
expect(typeof body['blockHeight']).toBe('number')
858+
expect(typeof body['tac']).toBe('string')
859+
expect(TXID_RE.test(body['tac'] as string)).toBe(true)
860+
}
861+
}
862+
}
863+
864+
function dispatchBRC136(input: Record<string, unknown>, expected: Record<string, unknown>): void {
865+
const method = getString(input, 'method')
866+
if (method !== '') {
867+
dispatchBRC136HTTP(input, expected)
868+
return
869+
}
870+
871+
const channel = getString(input, 'channel')
872+
if (channel === 'basm/root') {
873+
const cases = input['cases'] as Array<{ name: string, txids: string[] }>
874+
const roots = (expected['roots'] ?? {}) as Record<string, string>
875+
for (const testCase of cases) {
876+
expect(computeBasmRootForVector(testCase.txids)).toBe(roots[testCase.name])
877+
}
878+
return
879+
}
880+
881+
if (channel === 'basm/tac') {
882+
expect(computeTacForVector(
883+
getString(input, 'prevTac'),
884+
getString(input, 'blockHash'),
885+
getString(input, 'basmRoot')
886+
)).toBe(expected['tac'])
887+
return
888+
}
889+
890+
if (channel === 'basm/rawTransactions') {
891+
const msg = (input['message'] ?? {}) as Record<string, unknown>
892+
expect(Array.isArray(msg['transactions'])).toBe(true)
893+
for (const record of msg['transactions'] as unknown[]) {
894+
expect(typeof record).toBe('object')
895+
expect(record).not.toBeNull()
896+
const tx = record as Record<string, unknown>
897+
expect(typeof tx['txid']).toBe('string')
898+
expect(TXID_RE.test(tx['txid'] as string)).toBe(true)
899+
expect(typeof tx['rawTx']).toBe('string')
900+
expect(/^[0-9a-fA-F]+$/.test(tx['rawTx'] as string)).toBe(true)
901+
}
902+
expect(Array.isArray(msg['missing'])).toBe(true)
903+
expect(expected['valid']).toBe(true)
904+
return
905+
}
906+
907+
throw new Error(`sync dispatcher: unknown BRC-136 BASM channel '${channel}'`)
908+
}
909+
790910
// ── Main dispatch entry point ──────────────────────────────────────────────────
791911

792912
export function dispatch (
@@ -802,6 +922,10 @@ export function dispatch (
802922
dispatchChaintracksV2(input, expected)
803923
return
804924
}
925+
if (category === 'brc136-basm') {
926+
dispatchBRC136(input, expected)
927+
return
928+
}
805929
if (category !== 'gasp-protocol') {
806930
throw new Error(`sync dispatcher: unknown category '${category}'`)
807931
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
{
2+
"$schema": "../../schema/vector.schema.json",
3+
"id": "sync.brc136-basm",
4+
"name": "BRC-136 BASM anchor vectors",
5+
"brc": ["BRC-136"],
6+
"version": "1.0.0",
7+
"reference_impl": "packages/overlays/overlay/src/BASM.ts",
8+
"parity_class": "required",
9+
"vectors": [
10+
{
11+
"id": "sync.brc136-basm.1",
12+
"description": "BASM root edge cases and odd-leaf duplication",
13+
"input": {
14+
"channel": "basm/root",
15+
"cases": [
16+
{
17+
"name": "empty",
18+
"txids": []
19+
},
20+
{
21+
"name": "single",
22+
"txids": ["0101010101010101010101010101010101010101010101010101010101010101"]
23+
},
24+
{
25+
"name": "two",
26+
"txids": [
27+
"0101010101010101010101010101010101010101010101010101010101010101",
28+
"0202020202020202020202020202020202020202020202020202020202020202"
29+
]
30+
},
31+
{
32+
"name": "odd",
33+
"txids": [
34+
"0101010101010101010101010101010101010101010101010101010101010101",
35+
"0202020202020202020202020202020202020202020202020202020202020202",
36+
"0303030303030303030303030303030303030303030303030303030303030303"
37+
]
38+
}
39+
]
40+
},
41+
"expected": {
42+
"roots": {
43+
"empty": "0000000000000000000000000000000000000000000000000000000000000000",
44+
"single": "0101010101010101010101010101010101010101010101010101010101010101",
45+
"two": "b4100303b9e99ada4b479b0bb93d9b549cb057a1c4be08896bc982debe20ce39",
46+
"odd": "acbd47d5022a6c5e954ad677df8ec221c893f871889826df53f0f1ad3f023e22"
47+
}
48+
},
49+
"tags": ["basm", "root", "byte-order", "odd-leaf"]
50+
},
51+
{
52+
"id": "sync.brc136-basm.2",
53+
"description": "TAC chaining uses display-order inputs but hashes internal bytes",
54+
"input": {
55+
"channel": "basm/tac",
56+
"prevTac": "0000000000000000000000000000000000000000000000000000000000000000",
57+
"blockHash": "0404040404040404040404040404040404040404040404040404040404040404",
58+
"basmRoot": "acbd47d5022a6c5e954ad677df8ec221c893f871889826df53f0f1ad3f023e22"
59+
},
60+
"expected": {
61+
"tac": "d0d3e770802c7c28ca1da59cc92263d59425f00e48d421ffebb02497b4702307"
62+
},
63+
"tags": ["basm", "tac", "byte-order"]
64+
},
65+
{
66+
"id": "sync.brc136-basm.3",
67+
"description": "BASM HTTP route shapes",
68+
"input": {
69+
"method": "POST",
70+
"path": "/requestTopicAnchorTip",
71+
"headers": {
72+
"x-bsv-topic": "tm_example"
73+
},
74+
"body": {}
75+
},
76+
"expected": {
77+
"status": 200,
78+
"body": {
79+
"topic": "tm_example",
80+
"blockHeight": 850000,
81+
"tac": "d0d3e770802c7c28ca1da59cc92263d59425f00e48d421ffebb02497b4702307"
82+
}
83+
},
84+
"tags": ["basm", "http", "tip"]
85+
},
86+
{
87+
"id": "sync.brc136-basm.4",
88+
"description": "Raw transaction response shape for BEEF-free BASM catch-up",
89+
"input": {
90+
"channel": "basm/rawTransactions",
91+
"message": {
92+
"transactions": [
93+
{
94+
"txid": "0101010101010101010101010101010101010101010101010101010101010101",
95+
"rawTx": "01000000000000000000"
96+
}
97+
],
98+
"missing": []
99+
}
100+
},
101+
"expected": {
102+
"valid": true
103+
},
104+
"tags": ["basm", "raw-tx"]
105+
}
106+
]
107+
}

0 commit comments

Comments
 (0)