Skip to content

Commit 0308028

Browse files
authored
Merge pull request #170 from ty-everett/codex/brc136-basm-sync
[codex] Implement BRC-136 BASM overlay sync
2 parents e9d307d + 0803b7d commit 0308028

86 files changed

Lines changed: 4227 additions & 586 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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ jobs:
8080
if [ -n "$SCOPE_FILTER" ]; then EXTRA="--filter $SCOPE_FILTER"; fi
8181
pnpm -r --filter '!@bsv/ts-stack' $EXTRA run lint --if-present
8282
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+
8394
- name: Test changed packages
8495
# Exclude: workspace root (delegating script) and example-paymail (example package, not production)
8596
env:

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)