@@ -132,6 +132,11 @@ import { fileURLToPath } from 'node:url';
132132import { promisify } from 'node:util' ;
133133
134134import { schnorr , secp256k1 } from '@noble/curves/secp256k1' ;
135+ import {
136+ npubEncode , p2trAddressEncode , markStateHash , trailProgram , trailAddress , npubShort ,
137+ } from './lib/blocktrails.js' ;
138+ // re-export the four the test suite imports from plugin.js (back-compat)
139+ export { markStateHash , npubEncode , trailAddress , trailProgram } from './lib/blocktrails.js' ;
135140
136141const execFileP = promisify ( execFile ) ;
137142
@@ -324,126 +329,8 @@ function nostrHexOf(agent) {
324329 return m ? m [ 1 ] : null ;
325330}
326331
327- // bech32 (BIP-173, full checksum — no shortcuts) npub encoder, pure node.
328- // Unit-tested against the canonical NIP-19 vector in test.js. Tier 3.5
329- // generalizes the same polymod into a bech32m (BIP-350) P2TR address
330- // encoder — the two specs differ ONLY by the checksum constant
331- // (bech32 xors 1, bech32m xors 0x2bc830a3).
332- const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' ;
333- const BECH32M_CONST = 0x2bc830a3 ;
334-
335- function bech32Polymod ( values ) {
336- const GEN = [ 0x3b6a57b2 , 0x26508e6d , 0x1ea119fa , 0x3d4233dd , 0x2a1462b3 ] ;
337- let chk = 1 ;
338- for ( const v of values ) {
339- const top = chk >>> 25 ;
340- chk = ( ( chk & 0x1ffffff ) << 5 ) ^ v ;
341- for ( let i = 0 ; i < 5 ; i ++ ) if ( ( top >>> i ) & 1 ) chk ^= GEN [ i ] ;
342- }
343- return chk ;
344- }
345-
346- /** 8-bit bytes regrouped big-endian into 5-bit words (padded). */
347- function to5bit ( bytes ) {
348- const words = [ ] ;
349- let acc = 0 ;
350- let bits = 0 ;
351- for ( const b of bytes ) {
352- acc = ( acc << 8 ) | b ;
353- bits += 8 ;
354- while ( bits >= 5 ) { bits -= 5 ; words . push ( ( acc >>> bits ) & 31 ) ; }
355- }
356- if ( bits ) words . push ( ( acc << ( 5 - bits ) ) & 31 ) ;
357- return words ;
358- }
359-
360- /** hrp + data words + 6-word checksum (constant picks bech32 vs bech32m). */
361- function bech32Assemble ( hrp , words , constant ) {
362- const expanded = [ ...hrp ] . map ( ( c ) => c . charCodeAt ( 0 ) >>> 5 )
363- . concat ( [ 0 ] , [ ...hrp ] . map ( ( c ) => c . charCodeAt ( 0 ) & 31 ) ) ;
364- const poly = bech32Polymod ( [ ...expanded , ...words , 0 , 0 , 0 , 0 , 0 , 0 ] ) ^ constant ;
365- const checksum = Array . from ( { length : 6 } , ( _ , i ) => ( poly >>> ( 5 * ( 5 - i ) ) ) & 31 ) ;
366- return `${ hrp } 1${ [ ...words , ...checksum ] . map ( ( d ) => BECH32_CHARSET [ d ] ) . join ( '' ) } ` ;
367- }
368-
369- /** 64-hex pubkey -> npub1... (NIP-19). Exported for the vector test. */
370- export function npubEncode ( hex ) {
371- return bech32Assemble ( 'npub' , to5bit ( Buffer . from ( hex , 'hex' ) ) , 1 ) ;
372- }
373-
374- /** 32-byte P2TR witness program -> bech32m address (BIP-350, version 1). */
375- export function p2trAddressEncode ( hrp , program ) {
376- return bech32Assemble ( hrp , [ 1 , ...to5bit ( program ) ] , BECH32M_CONST ) ;
377- }
378-
379- // ------------------------------------------- Blocktrails trail math (3.5)
380- // Implemented FROM THE SPEC (blocktrails spec v0.2 — the mirror at
381- // blocktrails/spec/spec.md is normative): chained BIP-341 TapTweak.
382- //
383- // h = sha256(serialize(state)) state hash
384- // tᵢ = tagged_hash("TapTweak", x_only(Pᵢ₋₁) || h) mod n
385- // Pᵢ = Pᵢ₋₁ + tᵢ·G full point kept
386- // output = x_only(Pᵢ) → bech32m (tb1p…) x-only at the boundary
387- //
388- // serialize(state) is the git-mark-demo convention:
389- // JSON.stringify({ commit, repo, branch }) — canonical because the object
390- // is built literally in that key order everywhere it is hashed.
391- // Parity note (a Finding): the spec keeps FULL points through the chain
392- // and takes x_only only at the output boundary — x(P) == x(-P) makes the
393- // output comparison parity-free, and the tweak input is the raw
394- // x-coordinate of the (possibly odd-Y) running point, NOT a re-lifted
395- // even-Y key as in BIP-341 wallet derivation. Cross-checked in test.js
396- // against a vector generated by the maintainer's reference implementation.
397- // The blocktrails npm package is deliberately not imported (spec cited,
398- // math local); @noble/curves does the point arithmetic.
399-
400- const SECP_N = secp256k1 . CURVE . n ;
401- const sha256Of = ( ...parts ) => {
402- const h = crypto . createHash ( 'sha256' ) ;
403- for ( const p of parts ) h . update ( p ) ;
404- return h . digest ( ) ;
405- } ;
406- const TAPTWEAK_TAG = sha256Of ( Buffer . from ( 'TapTweak' , 'utf8' ) ) ;
407-
408- /** Canonical mark state hash: sha256 hex of the state JSON. Exported for tests. */
409- export function markStateHash ( state ) {
410- return crypto . createHash ( 'sha256' ) . update ( JSON . stringify ( state ) , 'utf8' ) . digest ( 'hex' ) ;
411- }
412-
413- /** BIP-341 TapTweak scalar over (x_only(P), stateHash), in [1, n-1]. */
414- function tapTweakScalar ( xOnly , stateHashBytes ) {
415- const t = BigInt ( `0x${ sha256Of ( TAPTWEAK_TAG , TAPTWEAK_TAG , xOnly , stateHashBytes ) . toString ( 'hex' ) } ` ) % SECP_N ;
416- // The spec REQUIRES rejecting t = 0 (probability ~2^-256).
417- if ( t === 0n ) throw new Error ( 'blocktrails: TapTweak scalar is zero — state rejected' ) ;
418- return t ;
419- }
420-
421- /** Chain the tweaks: base pubkey + one TapTweak per state hash (hex list). */
422- function trailPoint ( pubkeyBaseHex , stateHashes ) {
423- let P = secp256k1 . ProjectivePoint . fromHex ( pubkeyBaseHex ) ;
424- for ( const h of stateHashes ) {
425- const xOnly = P . toRawBytes ( true ) . subarray ( 1 ) ; // raw x of the running point
426- const t = tapTweakScalar ( xOnly , Buffer . from ( h , 'hex' ) ) ;
427- P = P . add ( secp256k1 . ProjectivePoint . BASE . multiply ( t ) ) ;
428- }
429- return P ;
430- }
431-
432- /** Witness program (x-only, 64-hex) after chaining stateHashes. Exported for tests. */
433- export function trailProgram ( pubkeyBaseHex , stateHashes ) {
434- return Buffer . from ( trailPoint ( pubkeyBaseHex , stateHashes ) . toRawBytes ( true ) . subarray ( 1 ) ) . toString ( 'hex' ) ;
435- }
436-
437- /** Derived P2TR address after chaining stateHashes. Exported for tests. */
438- export function trailAddress ( pubkeyBaseHex , stateHashes , hrp ) {
439- return p2trAddressEncode ( hrp , Buffer . from ( trailProgram ( pubkeyBaseHex , stateHashes ) , 'hex' ) ) ;
440- }
441-
442- /** Shortened npub for UI rendering: npub1abcd…wxyz. */
443- function npubShort ( hex ) {
444- const npub = npubEncode ( hex ) ;
445- return `${ npub . slice ( 0 , 9 ) } …${ npub . slice ( - 4 ) } ` ;
446- }
332+ // bech32 (npub) + bech32m (P2TR) + Blocktrails TapTweak trail math moved to
333+ // ./lib/blocktrails.js (pure, no forge state) — imported & re-exported above.
447334
448335/** Agent -> pod username OR 64-hex nostr namespace (2.5), or null. */
449336function ownerFromAgent ( agent ) {
0 commit comments