Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/optimize-nip03-nip05-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"nostream": patch
---

test: optimize nip05.spec.ts & nip03.spec.ts resource management

- Lift sinon stub to `before`/`after` in verifyNip05Identifier tests (create once, reset between tests)
- Extract SSRF guard callback once in `before` instead of per-test `beforeEach`
- Pre-build shared OTS buffers and attestations at module scope to eliminate redundant Buffer.concat calls
- Add shared event factory for extractNip05FromEvent tests
97 changes: 56 additions & 41 deletions test/unit/utils/nip03.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,42 @@ function buildOts(digest: Buffer, subItems: Buffer[]): Buffer {
const EVENT_ID = 'e71c6ea722987debdb60f81f9ea4f604b5ac0664120dd64fb9d23abc4ec7c323'
const DIGEST = Buffer.from(EVENT_ID, 'hex')

const BITCOIN_ATTESTATION_810391 = bitcoinAttestation(810391)
const BITCOIN_ATTESTATION_1 = bitcoinAttestation(1)
const BITCOIN_ATTESTATION_42 = bitcoinAttestation(42)
const PENDING_ATTESTATION_DEFAULT = pendingAttestation('https://a.pool.opentimestamps.org')

const MINIMAL_BITCOIN_OTS = buildOts(DIGEST, [BITCOIN_ATTESTATION_810391])

const OTS_WITH_APPEND_OP = (() => {
const opAppend = Buffer.concat([Buffer.from([OP_APPEND]), writeVarBytes(Buffer.from([0xde, 0xad, 0xbe, 0xef]))])
const tree = Buffer.concat([opAppend, BITCOIN_ATTESTATION_1])
return buildOts(DIGEST, [tree])
})()

const OTS_PENDING_AND_BITCOIN = buildOts(DIGEST, [PENDING_ATTESTATION_DEFAULT, BITCOIN_ATTESTATION_42])

const OTS_LITECOIN_AND_UNKNOWN = buildOts(DIGEST, [litecoinAttestation(2500000), unknownAttestation(Buffer.from([1, 2, 3]))])

const OTS_ETHEREUM = buildOts(DIGEST, [ethereumAttestation(18_000_000)])

const OTS_PREPEND = (() => {
const prepend = Buffer.concat([Buffer.from([OP_PREPEND]), writeVarBytes(Buffer.from([0x01]))])
return buildOts(DIGEST, [Buffer.concat([prepend, bitcoinAttestation(4)])])
})()

const OTS_REVERSE_HEXLIFY = (() => {
const revThenHex = Buffer.concat([Buffer.from([OP_REVERSE]), Buffer.from([OP_HEXLIFY]), bitcoinAttestation(5)])
return buildOts(DIGEST, [revThenHex])
})()

const OTS_TRUNCATED_ATTESTATION = (() => {
const broken = Buffer.concat([Buffer.from([TAG_ATTESTATION]), BITCOIN_TAG, writeVarUint(0)])
return buildOts(DIGEST, [broken])
})()

const VALID_PROOF_BASE64 = MINIMAL_BITCOIN_OTS.toString('base64')

describe('OtsReader', () => {
it('readBytes rejects a negative length', () => {
const r = new OtsReader(Buffer.from([1, 2, 3]))
Expand All @@ -166,9 +202,7 @@ describe('OtsReader', () => {
describe('NIP-03 — OpenTimestamps', () => {
describe('parseOtsFile', () => {
it('parses a minimal proof with a single bitcoin attestation', () => {
const buf = buildOts(DIGEST, [bitcoinAttestation(810391)])

const result = expectSuccess(parseOtsFile(buf))
const result = expectSuccess(parseOtsFile(MINIMAL_BITCOIN_OTS))

expect(result.summary.version).to.equal(1)
expect(result.summary.fileHashOp).to.equal('sha256')
Expand All @@ -178,27 +212,19 @@ describe('NIP-03 — OpenTimestamps', () => {
})

it('parses a proof with ops that wrap an attestation', () => {
const opAppend = Buffer.concat([Buffer.from([OP_APPEND]), writeVarBytes(Buffer.from([0xde, 0xad, 0xbe, 0xef]))])
const tree = Buffer.concat([opAppend, bitcoinAttestation(1)])
const buf = buildOts(DIGEST, [tree])

const result = expectSuccess(parseOtsFile(buf))
const result = expectSuccess(parseOtsFile(OTS_WITH_APPEND_OP))

expect(result.summary.attestations.map((a) => a.kind)).to.deep.equal(['bitcoin'])
})

it('parses a proof with multiple attestations (pending + bitcoin)', () => {
const buf = buildOts(DIGEST, [pendingAttestation('https://a.pool.opentimestamps.org'), bitcoinAttestation(42)])

const result = expectSuccess(parseOtsFile(buf))
const result = expectSuccess(parseOtsFile(OTS_PENDING_AND_BITCOIN))
const kinds = result.summary.attestations.map((a) => a.kind).sort()
expect(kinds).to.deep.equal(['bitcoin', 'pending'])
})

it('classifies litecoin and unknown attestations correctly', () => {
const buf = buildOts(DIGEST, [litecoinAttestation(2500000), unknownAttestation(Buffer.from([1, 2, 3]))])

const result = expectSuccess(parseOtsFile(buf))
const result = expectSuccess(parseOtsFile(OTS_LITECOIN_AND_UNKNOWN))
const kinds = result.summary.attestations.map((a) => a.kind).sort()
expect(kinds).to.deep.equal(['litecoin', 'unknown'])
})
Expand All @@ -210,28 +236,26 @@ describe('NIP-03 — OpenTimestamps', () => {
})

it('rejects an unsupported file hash op', () => {
const parts = [MAGIC, writeVarUint(1), Buffer.from([0x55]), Buffer.alloc(32), bitcoinAttestation(1)]
const parts = [MAGIC, writeVarUint(1), Buffer.from([0x55]), Buffer.alloc(32), BITCOIN_ATTESTATION_1]
const buf = Buffer.concat(parts)
const result = expectFailure(parseOtsFile(buf))
expect(result.reason).to.match(/unsupported file hash op/)
})

it('rejects an unsupported ots file version', () => {
const parts = [MAGIC, writeVarUint(2), Buffer.from([OP_SHA256]), DIGEST, bitcoinAttestation(1)]
const parts = [MAGIC, writeVarUint(2), Buffer.from([OP_SHA256]), DIGEST, BITCOIN_ATTESTATION_1]
const buf = Buffer.concat(parts)
const result = expectFailure(parseOtsFile(buf))
expect(result.reason).to.match(/unsupported ots version/)
})

it('rejects truncated proofs without crashing', () => {
const good = buildOts(DIGEST, [bitcoinAttestation(1)])
const truncated = good.subarray(0, good.length - 3)
const truncated = MINIMAL_BITCOIN_OTS.subarray(0, MINIMAL_BITCOIN_OTS.length - 3)
expectFailure(parseOtsFile(truncated))
})

it('rejects proofs with trailing garbage', () => {
const good = buildOts(DIGEST, [bitcoinAttestation(1)])
const withGarbage = Buffer.concat([good, Buffer.from([0x00, 0x11, 0x22])])
const withGarbage = Buffer.concat([MINIMAL_BITCOIN_OTS, Buffer.from([0x00, 0x11, 0x22])])
const result = expectFailure(parseOtsFile(withGarbage))
expect(result.reason).to.match(/trailing bytes/)
})
Expand All @@ -252,7 +276,7 @@ describe('NIP-03 — OpenTimestamps', () => {

it('parses sha1 file digest (20-byte) proofs', () => {
const digest = Buffer.alloc(20, 0xab)
const buf = buildOtsWithFileHashOp(OP_SHA1, digest, [bitcoinAttestation(1)])
const buf = buildOtsWithFileHashOp(OP_SHA1, digest, [BITCOIN_ATTESTATION_1])
const result = expectSuccess(parseOtsFile(buf))
expect(result.summary.fileHashOp).to.equal('sha1')
expect(result.summary.digest).to.equal(digest.toString('hex'))
Expand All @@ -273,31 +297,24 @@ describe('NIP-03 — OpenTimestamps', () => {
})

it('parses prepend binary op in the commitment tree', () => {
const prepend = Buffer.concat([Buffer.from([OP_PREPEND]), writeVarBytes(Buffer.from([0x01]))])
const buf = buildOts(DIGEST, [Buffer.concat([prepend, bitcoinAttestation(4)])])
const result = expectSuccess(parseOtsFile(buf))
const result = expectSuccess(parseOtsFile(OTS_PREPEND))
expect(result.summary.attestations.some((a) => a.kind === 'bitcoin')).to.equal(true)
})

it('parses reverse and hexlify unary ops wrapping an attestation', () => {
const revThenHex = Buffer.concat([Buffer.from([OP_REVERSE]), Buffer.from([OP_HEXLIFY]), bitcoinAttestation(5)])
const buf = buildOts(DIGEST, [revThenHex])
const result = expectSuccess(parseOtsFile(buf))
const result = expectSuccess(parseOtsFile(OTS_REVERSE_HEXLIFY))
expect(result.summary.attestations[0].kind).to.equal('bitcoin')
})

it('classifies ethereum block header attestations', () => {
const buf = buildOts(DIGEST, [ethereumAttestation(18_000_000)])
const result = expectSuccess(parseOtsFile(buf))
const result = expectSuccess(parseOtsFile(OTS_ETHEREUM))
const eth = result.summary.attestations.find((a) => a.kind === 'ethereum')
expect(eth).to.exist
expect(eth?.height).to.equal(18_000_000)
})

it('treats a truncated bitcoin attestation payload as height-less', () => {
const broken = Buffer.concat([Buffer.from([TAG_ATTESTATION]), BITCOIN_TAG, writeVarUint(0)])
const buf = buildOts(DIGEST, [broken])
const result = expectSuccess(parseOtsFile(buf))
const result = expectSuccess(parseOtsFile(OTS_TRUNCATED_ATTESTATION))
expect(result.summary.attestations[0]).to.include({ kind: 'bitcoin' })
expect(result.summary.attestations[0].height).to.equal(undefined)
})
Expand Down Expand Up @@ -343,14 +360,12 @@ describe('NIP-03 — OpenTimestamps', () => {
})

describe('validateOtsProof', () => {
const validProof = buildOts(DIGEST, [bitcoinAttestation(810391)]).toString('base64')

it('accepts a well-formed bitcoin-anchored proof whose digest matches', () => {
expect(validateOtsProof(validProof, EVENT_ID)).to.equal(undefined)
expect(validateOtsProof(VALID_PROOF_BASE64, EVENT_ID)).to.equal(undefined)
})

it('accepts uppercase hex target ids and normalizes them', () => {
expect(validateOtsProof(validProof, EVENT_ID.toUpperCase())).to.equal(undefined)
expect(validateOtsProof(VALID_PROOF_BASE64, EVENT_ID.toUpperCase())).to.equal(undefined)
})

it('rejects empty content', () => {
Expand All @@ -363,26 +378,26 @@ describe('NIP-03 — OpenTimestamps', () => {

it('rejects proofs whose digest does not match the event id', () => {
const other = '0'.repeat(64)
expect(validateOtsProof(validProof, other)).to.match(/digest does not match/)
expect(validateOtsProof(VALID_PROOF_BASE64, other)).to.match(/digest does not match/)
})

it('rejects target ids that are not 32-byte hex', () => {
expect(validateOtsProof(validProof, 'not-an-id')).to.match(/not a 32-byte hex/)
expect(validateOtsProof(VALID_PROOF_BASE64, 'not-an-id')).to.match(/not a 32-byte hex/)
})

it('rejects a non-string target event id', () => {
expect(validateOtsProof(validProof, null as unknown as string)).to.match(/not a 32-byte hex/)
expect(validateOtsProof(VALID_PROOF_BASE64, null as unknown as string)).to.match(/not a 32-byte hex/)
})

it('rejects proofs without any bitcoin attestation', () => {
const onlyPending = buildOts(DIGEST, [pendingAttestation('https://a.pool.opentimestamps.org')]).toString('base64')
const onlyPending = buildOts(DIGEST, [PENDING_ATTESTATION_DEFAULT]).toString('base64')
expect(validateOtsProof(onlyPending, EVENT_ID)).to.match(/bitcoin attestation/)
})

it('rejects a proof digested with a non-sha256 hash op', () => {
// Construct a manual RIPEMD160 file (20-byte digest) — this should fail
// the sha256 requirement even though the parser would otherwise accept it.
const parts = [MAGIC, writeVarUint(1), Buffer.from([0x03]), Buffer.alloc(20, 0xaa), bitcoinAttestation(1)]
const parts = [MAGIC, writeVarUint(1), Buffer.from([0x03]), Buffer.alloc(20, 0xaa), BITCOIN_ATTESTATION_1]
const ripemd = Buffer.concat(parts).toString('base64')
expect(validateOtsProof(ripemd, 'aa'.repeat(32))).to.match(/sha256 file hash op/)
})
Expand Down
108 changes: 44 additions & 64 deletions test/unit/utils/nip05.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ import { EventKinds } from '../../../src/constants/base'

const { expect } = chai

const PUBKEY_A = 'a'.repeat(64)
const PUBKEY_B = 'b'.repeat(64)
const SIG_C = 'c'.repeat(128)
const ID_A = PUBKEY_A

function makeEvent(kind: number, content: string): Event {
return {
id: ID_A,
pubkey: PUBKEY_B,
created_at: 1234567890,
kind,
tags: [],
content,
sig: SIG_C,
}
}

describe('NIP-05 utils', () => {
describe('parseNip05Identifier', () => {
it('returns parsed identifier for valid input', () => {
Expand Down Expand Up @@ -75,81 +92,39 @@ describe('NIP-05 utils', () => {

describe('extractNip05FromEvent', () => {
it('extracts nip05 from kind 0 event', () => {
const event: Event = {
id: 'a'.repeat(64),
pubkey: 'b'.repeat(64),
created_at: 1234567890,
kind: EventKinds.SET_METADATA,
tags: [],
content: JSON.stringify({ name: 'alice', nip05: 'alice@example.com' }),
sig: 'c'.repeat(128),
}
expect(extractNip05FromEvent(event)).to.equal('alice@example.com')
expect(extractNip05FromEvent(
makeEvent(EventKinds.SET_METADATA, JSON.stringify({ name: 'alice', nip05: 'alice@example.com' })),
)).to.equal('alice@example.com')
})

it('returns undefined for non-kind-0 event', () => {
const event: Event = {
id: 'a'.repeat(64),
pubkey: 'b'.repeat(64),
created_at: 1234567890,
kind: EventKinds.TEXT_NOTE,
tags: [],
content: JSON.stringify({ nip05: 'alice@example.com' }),
sig: 'c'.repeat(128),
}
expect(extractNip05FromEvent(event)).to.be.undefined
expect(extractNip05FromEvent(
makeEvent(EventKinds.TEXT_NOTE, JSON.stringify({ nip05: 'alice@example.com' })),
)).to.be.undefined
})

it('returns undefined when nip05 is not in content', () => {
const event: Event = {
id: 'a'.repeat(64),
pubkey: 'b'.repeat(64),
created_at: 1234567890,
kind: EventKinds.SET_METADATA,
tags: [],
content: JSON.stringify({ name: 'alice' }),
sig: 'c'.repeat(128),
}
expect(extractNip05FromEvent(event)).to.be.undefined
expect(extractNip05FromEvent(
makeEvent(EventKinds.SET_METADATA, JSON.stringify({ name: 'alice' })),
)).to.be.undefined
})

it('returns undefined for invalid JSON content', () => {
const event: Event = {
id: 'a'.repeat(64),
pubkey: 'b'.repeat(64),
created_at: 1234567890,
kind: EventKinds.SET_METADATA,
tags: [],
content: 'not json',
sig: 'c'.repeat(128),
}
expect(extractNip05FromEvent(event)).to.be.undefined
expect(extractNip05FromEvent(
makeEvent(EventKinds.SET_METADATA, 'not json'),
)).to.be.undefined
})

it('returns undefined when nip05 is empty string', () => {
const event: Event = {
id: 'a'.repeat(64),
pubkey: 'b'.repeat(64),
created_at: 1234567890,
kind: EventKinds.SET_METADATA,
tags: [],
content: JSON.stringify({ nip05: '' }),
sig: 'c'.repeat(128),
}
expect(extractNip05FromEvent(event)).to.be.undefined
expect(extractNip05FromEvent(
makeEvent(EventKinds.SET_METADATA, JSON.stringify({ nip05: '' })),
)).to.be.undefined
})

it('returns undefined when nip05 is not a string', () => {
const event: Event = {
id: 'a'.repeat(64),
pubkey: 'b'.repeat(64),
created_at: 1234567890,
kind: EventKinds.SET_METADATA,
tags: [],
content: JSON.stringify({ nip05: 42 }),
sig: 'c'.repeat(128),
}
expect(extractNip05FromEvent(event)).to.be.undefined
expect(extractNip05FromEvent(
makeEvent(EventKinds.SET_METADATA, JSON.stringify({ nip05: 42 })),
)).to.be.undefined
})
})

Expand Down Expand Up @@ -190,13 +165,17 @@ describe('NIP-05 utils', () => {

describe('verifyNip05Identifier', () => {
let axiosGetStub: Sinon.SinonStub
const pubkey = 'a'.repeat(64)
const pubkey = PUBKEY_A

beforeEach(() => {
before(() => {
axiosGetStub = Sinon.stub(axios, 'get')
})

afterEach(() => {
axiosGetStub.reset()
})

after(() => {
axiosGetStub.restore()
})

Expand Down Expand Up @@ -241,7 +220,7 @@ describe('NIP-05 utils', () => {
})

it('returns mismatch when pubkey does not match', async () => {
axiosGetStub.resolves({ data: { names: { alice: 'b'.repeat(64) } } })
axiosGetStub.resolves({ data: { names: { alice: PUBKEY_B } } })

const outcome = await verifyNip05Identifier('alice@example.com', pubkey)

Expand Down Expand Up @@ -286,7 +265,8 @@ describe('NIP-05 utils', () => {
describe('beforeRedirect SSRF guard', () => {
let guard: (options: { href?: string; protocol?: string; hostname?: string }) => void

beforeEach(async () => {
before(async () => {
axiosGetStub.reset()
axiosGetStub.resolves({ data: { names: { alice: pubkey } } })
await verifyNip05Identifier('alice@example.com', pubkey)
guard = axiosGetStub.firstCall.args[1].beforeRedirect
Expand Down
Loading