OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / blind-mint.test.js

98 lines · javascript ref:
import test from 'brittle'
import sodium from 'sodium-universal'
import b4a from 'b4a'
import { secp256k1 } from '@noble/curves/secp256k1.js'
import { BlindMint, blind, unblind, newSecret, hashToCurve } from 'p2p-hiverelay/incentive/payment/blind-mint.js'

const Point = secp256k1.Point

function makeKeyPair () {
  const publicKey = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
  const secretKey = b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)
  sodium.crypto_sign_keypair(publicKey, secretKey)
  return { publicKey, secretKey }
}

// ─── Official Cashu NUT-00 test vectors — proof of interoperability ──

test('NUT-00 vector: hash_to_curve', (t) => {
  const vectors = [
    ['0000000000000000000000000000000000000000000000000000000000000000', '024cce997d3b518f739663b757deaec95bcd9473c30a14ac2fd04023a739d1a725'],
    ['0000000000000000000000000000000000000000000000000000000000000001', '022e7158e11c9506f1aa4248bf531298daa7febd6194f003edcd9b93ade6253acf'],
    ['0000000000000000000000000000000000000000000000000000000000000002', '026cdbe15362df59cd1dd3c9c11de8aedac2106eca69236ecd9fbe117af897be4f']
  ]
  for (const [msg, expected] of vectors) {
    t.is(hashToCurve(msg).toHex(true), expected, 'hash_to_curve(' + msg.slice(0, 4) + '…) matches spec')
  }
})

test('NUT-00 vector: blinded message B_ = Y + rG', (t) => {
  const vectors = [
    ['d341ee4871f1f889041e63cf0d3823c713eea6aff01e80f1719f08f9e5be98f6', '99fce58439fc37412ab3468b73db0569322588f62fb3a49182d67e23d877824a', '033b1a9737a40cc3fd9b6af4b723632b76a67a36782596304612a6c2bfb5197e6d'],
    ['f1aaf16c2239746f369572c0784d9dd3d032d952c2d992175873fb58fae31a60', 'f78476ea7cc9ade20f9e05e58a804cf19533f03ea805ece5fee88c8e2874ba50', '029bdf2d716ee366eddf599ba252786c1033f47e230248a4612a5670ab931f1763']
  ]
  for (const [x, r, expectedB] of vectors) {
    const Y = hashToCurve(x)
    const B_ = Y.add(Point.BASE.multiply(BigInt('0x' + r)))
    t.is(B_.toHex(true), expectedB, 'B_ matches spec')
  }
})

test('NUT-00 vector: blind signature C_ = k·B_', (t) => {
  // Mint key = 0x7f..7f, B_ from the spec, expected C_ from the spec.
  const mint = new BlindMint({ secret: '7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f' })
  const C_ = mint.blindSign('02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2')
  t.is(C_, '0398bc70ce8184d27ba89834d19f5199c84443c31131e48d3c1214db24247d005d', 'C_ matches spec')
})

// ─── Behaviour ──────────────────────────────────────────────────────

test('BDHKE: full blind → sign → unblind → verify round-trip', (t) => {
  const mint = new BlindMint({ keyPair: makeKeyPair() })
  const secret = newSecret()
  const { blinded, blindingFactor } = blind(secret)
  const C = unblind(mint.blindSign(blinded), blindingFactor, mint.publicKey)
  t.ok(mint.verifyToken(secret, C), 'unblinded token verifies against the mint')
})

test('BDHKE: a token from a different mint does not verify', (t) => {
  const mintA = new BlindMint({ keyPair: makeKeyPair() })
  const mintB = new BlindMint({ keyPair: makeKeyPair() })
  const secret = newSecret()
  const { blinded, blindingFactor } = blind(secret)
  const C = unblind(mintA.blindSign(blinded), blindingFactor, mintA.publicKey)
  t.ok(mintA.verifyToken(secret, C))
  t.absent(mintB.verifyToken(secret, C), 'cross-mint forgery rejected')
})

test('BDHKE: wrong secret does not verify against a valid signature', (t) => {
  const mint = new BlindMint({ keyPair: makeKeyPair() })
  const secret = newSecret()
  const { blinded, blindingFactor } = blind(secret)
  const C = unblind(mint.blindSign(blinded), blindingFactor, mint.publicKey)
  t.absent(mint.verifyToken(newSecret(), C), 'C is bound to its own secret')
})

test('BDHKE: mint rejects an invalid blinded message', (t) => {
  const mint = new BlindMint({ keyPair: makeKeyPair() })
  t.exception(() => mint.blindSign('00'.repeat(33)), /BLIND_INVALID_POINT/)
  t.exception(() => mint.blindSign('zz'), /BLIND_INVALID_POINT/)
})

test('BDHKE: deterministic mint key from keyPair is stable (survives restart)', (t) => {
  const kp = makeKeyPair()
  const a = new BlindMint({ keyPair: kp })
  const b = new BlindMint({ keyPair: kp })
  t.is(a.publicKey, b.publicKey, 'same keyPair → same mint key, no persistence needed')
  const secret = newSecret()
  const { blinded, blindingFactor } = blind(secret)
  const C = unblind(a.blindSign(blinded), blindingFactor, a.publicKey)
  t.ok(b.verifyToken(secret, C))
})

test('BDHKE: unlinkability — blinding the same secret twice yields distinct points', (t) => {
  const secret = newSecret()
  t.not(blind(secret).blinded, blind(secret).blinded, 'blinded messages differ (random r) → mint cannot link by B_')
  t.is(hashToCurve(secret).toHex(true), hashToCurve(secret).toHex(true), 'hash_to_curve is deterministic')
})