Files
minibits_wallet/__tests__/cashuDleq.test.ts
T

234 lines
6.2 KiB
TypeScript

import {
Amount,
blindMessage,
constructUnblindedSignature,
createBlindSignature,
createDLEQProof,
getPubKeyFromPrivKey,
pointFromBytes,
} from '@cashu/cashu-ts'
import type {MintKeys, Proof} from '@cashu/cashu-ts'
import {bytesToHex, hexToBytes} from '@noble/curves/utils.js'
jest.mock('../src/services/logService', () => ({
log: {
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
trace: jest.fn(),
warn: jest.fn(),
},
}))
jest.mock('../src/services/nostrService', () => ({
NostrClient: {
getFirstTagValue: jest.fn(),
},
}))
import {CashuUtils} from '../src/services/cashu/cashuUtils'
import AppError from '../src/utils/AppError'
const invalidDleqMessage = 'Offline ecash verification failed. Do not accept this token.'
const hexToNumber = (hex: string): bigint => BigInt(`0x${hex}`)
const numberToHexPadded64 = (scalar: bigint): string => scalar.toString(16).padStart(64, '0')
const makeProofFixture = function (): {proof: Proof; mintKeys: MintKeys[]} {
const privkey = hexToBytes('1'.padStart(64, '0'))
const pubkey = pointFromBytes(getPubKeyFromPrivKey(privkey))
const secret = new TextEncoder().encode('fakeSecret')
const r = hexToNumber('123456'.padStart(64, '0'))
const blindedMessage = blindMessage(secret, r)
const dleq = createDLEQProof(blindedMessage.B_, privkey)
const blindSignature = createBlindSignature(blindedMessage.B_, privkey, '00')
const unblinded = constructUnblindedSignature(blindSignature, r, secret, pubkey)
const proof: Proof = {
id: unblinded.id,
amount: Amount.from(1),
C: unblinded.C.toHex(true),
secret: new TextDecoder().decode(unblinded.secret),
dleq: {
r: numberToHexPadded64(r),
e: bytesToHex(dleq.e),
s: bytesToHex(dleq.s),
},
}
const mintKeys: MintKeys[] = [
{
id: unblinded.id,
unit: 'sat',
keys: {
'1': pubkey.toHex(true),
},
},
]
return {proof, mintKeys}
}
const expectValidationError = function (
run: () => void,
message: string,
) {
expect(run).toThrow(AppError)
try {
run()
} catch (e: any) {
expect(e.message).toBe(message)
}
}
describe('CashuUtils.verifyProofsDleqOrThrow', () => {
it('accepts a proof with valid DLEQ', () => {
const {proof, mintKeys} = makeProofFixture()
expect(() => CashuUtils.verifyProofsDleqOrThrow([proof], mintKeys)).not.toThrow()
})
it('rejects a proof without DLEQ', () => {
const {proof, mintKeys} = makeProofFixture()
const proofWithoutDleq: Proof = {...proof}
delete proofWithoutDleq.dleq
expectValidationError(
() => CashuUtils.verifyProofsDleqOrThrow([proofWithoutDleq], mintKeys),
'This token does not include offline verification proof. Receive it online instead.',
)
})
it('rejects a proof without the DLEQ blinding factor', () => {
const {proof, mintKeys} = makeProofFixture()
const proofWithoutR: Proof = {
...proof,
dleq: {
e: proof.dleq!.e,
s: proof.dleq!.s,
},
}
expectValidationError(
() => CashuUtils.verifyProofsDleqOrThrow([proofWithoutR], mintKeys),
'This token is missing the DLEQ blinding factor needed for offline verification.',
)
})
it('rejects a proof when the keyset is not cached', () => {
const {proof} = makeProofFixture()
expectValidationError(
() => CashuUtils.verifyProofsDleqOrThrow([proof], []),
'This token cannot be verified offline because the mint keys are not saved. Sync the mint online first.',
)
})
it('rejects a proof when the cached keyset does not have the amount key', () => {
const {proof, mintKeys} = makeProofFixture()
const keysWithoutAmount: MintKeys[] = [
{
...mintKeys[0],
keys: {
'2': mintKeys[0].keys['1'],
},
},
]
expectValidationError(
() => CashuUtils.verifyProofsDleqOrThrow([proof], keysWithoutAmount),
'This token cannot be verified offline because the mint keys are not saved. Sync the mint online first.',
)
})
it('rejects a proof with tampered DLEQ e', () => {
const {proof, mintKeys} = makeProofFixture()
const tamperedProof: Proof = {
...proof,
dleq: {
...proof.dleq!,
e: '00'.repeat(32),
},
}
expectValidationError(
() => CashuUtils.verifyProofsDleqOrThrow([tamperedProof], mintKeys),
invalidDleqMessage,
)
})
it('rejects a proof with tampered DLEQ s', () => {
const {proof, mintKeys} = makeProofFixture()
const tamperedProof: Proof = {
...proof,
dleq: {
...proof.dleq!,
s: '00'.repeat(32),
},
}
expectValidationError(
() => CashuUtils.verifyProofsDleqOrThrow([tamperedProof], mintKeys),
invalidDleqMessage,
)
})
it('rejects a proof with tampered DLEQ r', () => {
const {proof, mintKeys} = makeProofFixture()
const tamperedProof: Proof = {
...proof,
dleq: {
...proof.dleq!,
r: '01'.repeat(32),
},
}
expectValidationError(
() => CashuUtils.verifyProofsDleqOrThrow([tamperedProof], mintKeys),
invalidDleqMessage,
)
})
it('rejects a proof with tampered signature C', () => {
const {proof, mintKeys} = makeProofFixture()
const tamperedC = `${proof.C.startsWith('02') ? '03' : '02'}${proof.C.slice(2)}`
const tamperedProof: Proof = {
...proof,
C: tamperedC,
}
expect(tamperedProof.C).not.toBe(proof.C)
expectValidationError(
() => CashuUtils.verifyProofsDleqOrThrow([tamperedProof], mintKeys),
invalidDleqMessage,
)
})
it('rejects a proof with tampered secret', () => {
const {proof, mintKeys} = makeProofFixture()
const tamperedProof: Proof = {
...proof,
secret: `${proof.secret}-tampered`,
}
expectValidationError(
() => CashuUtils.verifyProofsDleqOrThrow([tamperedProof], mintKeys),
invalidDleqMessage,
)
})
it('rejects a multi-proof token when one proof has invalid DLEQ', () => {
const {proof, mintKeys} = makeProofFixture()
const tamperedProof: Proof = {
...proof,
secret: `${proof.secret}-tampered`,
}
expectValidationError(
() => CashuUtils.verifyProofsDleqOrThrow([proof, tamperedProof], mintKeys),
invalidDleqMessage,
)
})
})