3535 */
3636
3737import { expect } from '@jest/globals'
38+ import { createHash } from 'node:crypto'
3839
3940export 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). */
5153const TXID_RE = / ^ [ 0 - 9 a - f A - 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 - 9 a - f A - 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
792912export 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 }
0 commit comments