From 30445e16f32540325ac8593e6d0cd015a5e33232 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 29 Oct 2024 07:39:50 +0100 Subject: [PATCH 01/27] dleq --- package-lock.json | 124 +++++++++++++++--------------- package.json | 2 +- src/CashuWallet.ts | 105 ++++++++++++++++++++----- src/model/BlindedSignature.ts | 20 ++++- src/model/types/mint/responses.ts | 14 ++++ src/model/types/wallet/index.ts | 6 ++ 6 files changed, 189 insertions(+), 82 deletions(-) diff --git a/package-lock.json b/package-lock.json index 69d22b89..aee19c7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.1.0-3", "license": "MIT", "dependencies": { - "@cashu/crypto": "^0.2.7", + "@cashu/crypto": "^0.3.1", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", @@ -621,15 +621,14 @@ "dev": true }, "node_modules/@cashu/crypto": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.2.7.tgz", - "integrity": "sha512-1aaDfUjiHNXoJqg8nW+341TLWV9W28DsVNXJUKcHL0yAmwLs5+56SSnb8LLDJzPamLVoYL0U0bda91klAzptig==", - "license": "MIT", - "dependencies": { - "@noble/curves": "^1.3.0", - "@noble/hashes": "^1.3.3", - "@scure/bip32": "^1.3.3", - "@scure/bip39": "^1.2.2", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.1.tgz", + "integrity": "sha512-lxzUQgcz3lD/z2vFHgyPV5zZ/7kGdeJQDjvC5coReDD1eRRnZv1pBLEJTApGx3g0ZK3MD4ScCsLpnGJHNNq5sQ==", + "dependencies": { + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", "buffer": "^6.0.3" } }, @@ -1168,22 +1167,25 @@ } }, "node_modules/@noble/curves": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", - "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", + "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", "dependencies": { - "@noble/hashes": "1.3.3" + "@noble/hashes": "1.5.0" + }, + "engines": { + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", "engines": { - "node": ">= 16" + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -1225,33 +1227,33 @@ } }, "node_modules/@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.3.tgz", - "integrity": "sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.5.0.tgz", + "integrity": "sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==", "dependencies": { - "@noble/curves": "~1.3.0", - "@noble/hashes": "~1.3.2", - "@scure/base": "~1.1.4" + "@noble/curves": "~1.6.0", + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.7" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip39": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.2.tgz", - "integrity": "sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.4.0.tgz", + "integrity": "sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==", "dependencies": { - "@noble/hashes": "~1.3.2", - "@scure/base": "~1.1.4" + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.8" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -6833,14 +6835,14 @@ "dev": true }, "@cashu/crypto": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.2.7.tgz", - "integrity": "sha512-1aaDfUjiHNXoJqg8nW+341TLWV9W28DsVNXJUKcHL0yAmwLs5+56SSnb8LLDJzPamLVoYL0U0bda91klAzptig==", - "requires": { - "@noble/curves": "^1.3.0", - "@noble/hashes": "^1.3.3", - "@scure/bip32": "^1.3.3", - "@scure/bip39": "^1.2.2", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.1.tgz", + "integrity": "sha512-lxzUQgcz3lD/z2vFHgyPV5zZ/7kGdeJQDjvC5coReDD1eRRnZv1pBLEJTApGx3g0ZK3MD4ScCsLpnGJHNNq5sQ==", + "requires": { + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", "buffer": "^6.0.3" } }, @@ -7257,17 +7259,17 @@ } }, "@noble/curves": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", - "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", + "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", "requires": { - "@noble/hashes": "1.3.3" + "@noble/hashes": "1.5.0" } }, "@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==" }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -7296,27 +7298,27 @@ } }, "@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==" + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==" }, "@scure/bip32": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.3.tgz", - "integrity": "sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.5.0.tgz", + "integrity": "sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==", "requires": { - "@noble/curves": "~1.3.0", - "@noble/hashes": "~1.3.2", - "@scure/base": "~1.1.4" + "@noble/curves": "~1.6.0", + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.7" } }, "@scure/bip39": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.2.tgz", - "integrity": "sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.4.0.tgz", + "integrity": "sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==", "requires": { - "@noble/hashes": "~1.3.2", - "@scure/base": "~1.1.4" + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.8" } }, "@sinclair/typebox": { diff --git a/package.json b/package.json index 5e33c1f3..80ae4399 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "typescript": "^5.0.4" }, "dependencies": { - "@cashu/crypto": "^0.2.7", + "@cashu/crypto": "^0.3.1", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 2003b4e5..83970627 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -1,4 +1,4 @@ -import { bytesToHex, randomBytes } from '@noble/hashes/utils'; +import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils'; import { CashuMint } from './CashuMint.js'; import { BlindedMessage } from './model/BlindedMessage.js'; import { @@ -38,7 +38,8 @@ import { deriveSeedFromMnemonic } from '@cashu/crypto/modules/client/NUT09'; import { createP2PKsecret, getSignedProofs } from '@cashu/crypto/modules/client/NUT11'; -import { type Proof as NUT11Proof } from '@cashu/crypto/modules/common/index'; +import { type Proof as NUT11Proof, DLEQ } from '@cashu/crypto/modules/common/index'; +import { verifyDLEQProof_reblind } from '@cashu/crypto/modules/client/NUT12'; /** * The default number of proofs per denomination to keep in a wallet. @@ -276,6 +277,7 @@ class CashuWallet { * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param options.privkey? will create a signature on the @param tokenEntry secrets if set + * @param options.requireDLEQ? optionally require a DLEQ proof from the mint * @returns {Promise>} New token entry with newly created proofs, proofs that had errors */ async receiveTokenEntry( @@ -286,6 +288,7 @@ class CashuWallet { counter?: number; pubkey?: string; privkey?: string; + requireDLEQ?: boolean; } ): Promise> { const proofs: Array = []; @@ -303,11 +306,13 @@ class CashuWallet { options?.privkey ); const { signatures } = await this.mint.swap(payload); + const requireDleq = options?.requireDLEQ; const newProofs = this.constructProofs( signatures, blindingData.blindingFactors, blindingData.secrets, - keys + keys, + requireDleq ?? false ); proofs.push(...newProofs); return proofs; @@ -490,6 +495,7 @@ class CashuWallet { * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param options.privkey? will create a signature on the @param proofs secrets if set + * @param options.requireDLEQ? optionally require a DLEQ proof from the mint. * @returns promise of the change- and send-proofs */ async swap( @@ -503,6 +509,7 @@ class CashuWallet { privkey?: string; keysetId?: string; includeFees?: boolean; + requireDLEQ?: boolean; } ): Promise { if (!options) options = {}; @@ -575,11 +582,13 @@ class CashuWallet { options?.privkey ); const { signatures } = await this.mint.swap(payload); + const requireDleq = options?.requireDLEQ; const swapProofs = this.constructProofs( signatures, blindingData.blindingFactors, blindingData.secrets, - keyset + keyset, + requireDleq ?? false ); const splitProofsToKeep: Array = []; const splitProofsToSend: Array = []; @@ -603,6 +612,7 @@ class CashuWallet { * @param start set starting point for count (first cycle for each keyset should usually be 0) * @param count set number of blinded messages that should be generated * @param options.keysetId set a custom keysetId to restore from. keysetIds can be loaded with `CashuMint.getKeySets()` + * @param options.requireDLEQ require a DLEQ proof * @returns proofs */ async restore( @@ -610,6 +620,7 @@ class CashuWallet { count: number, options?: { keysetId?: string; + requireDLEQ?: boolean; } ): Promise<{ proofs: Array }> { const keys = await this.getKeys(options?.keysetId); @@ -633,9 +644,15 @@ class CashuWallet { const validSecrets = secrets.filter((_: Uint8Array, i: number) => outputs.map((o: SerializedBlindedMessage) => o.B_).includes(blindedMessages[i].B_) ); - + const requireDleq = options?.requireDLEQ; return { - proofs: this.constructProofs(promises, validBlindingFactors, validSecrets, keys) + proofs: this.constructProofs( + promises, + validBlindingFactors, + validSecrets, + keys, + requireDleq ?? false + ) }; } @@ -672,6 +689,7 @@ class CashuWallet { * @param options.outputAmounts? optionally specify the output's amounts to keep and to send. * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! + * @param options.requireDLEQ? optionally require a DLEQ proof. * @returns proofs */ async mintProofs( @@ -683,6 +701,7 @@ class CashuWallet { proofsWeHave?: Array; counter?: number; pubkey?: string; + requireDLEQ?: boolean; } ): Promise<{ proofs: Array }> { const keyset = await this.getKeys(options?.keysetId); @@ -710,8 +729,15 @@ class CashuWallet { quote: quote }; const { signatures } = await this.mint.mint(mintPayload); + const requireDleq = options?.requireDLEQ; return { - proofs: this.constructProofs(signatures, blindingFactors, secrets, keyset) + proofs: this.constructProofs( + signatures, + blindingFactors, + secrets, + keyset, + requireDleq ?? false + ) }; } @@ -756,6 +782,7 @@ class CashuWallet { keysetId?: string; counter?: number; privkey?: string; + requireDLEQ?: boolean; } ): Promise { const keys = await this.getKeys(options?.keysetId); @@ -784,8 +811,15 @@ class CashuWallet { }; const meltResponse = await this.mint.melt(meltPayload); let change: Array = []; + const requireDleq = options?.requireDLEQ; if (meltResponse.change) { - change = this.constructProofs(meltResponse.change, blindingFactors, secrets, keys); + change = this.constructProofs( + meltResponse.change, + blindingFactors, + secrets, + keys, + requireDleq ?? false + ); } return { quote: meltResponse, @@ -987,23 +1021,58 @@ class CashuWallet { * @param rs arrays of binding factors * @param secrets array of secrets * @param keyset mint keyset + * @param verifyDLEQ require proof of same secret (DLEQ) * @returns array of serialized proofs */ private constructProofs( promises: Array, rs: Array, secrets: Array, - keyset: MintKeys + keyset: MintKeys, + verifyDLEQ: boolean ): Array { - return promises - .map((p: SerializedBlindedSignature, i: number) => { - const blindSignature = { id: p.id, amount: p.amount, C_: pointFromHex(p.C_) }; - const r = rs[i]; - const secret = secrets[i]; - const A = pointFromHex(keyset.keys[p.amount]); - return constructProofFromPromise(blindSignature, r, secret, A); - }) - .map((p: NUT11Proof) => serializeProof(p) as Proof); + return promises.map((p: SerializedBlindedSignature, i: number) => { + const dleq = + p.dleq == undefined + ? undefined + : ({ + s: hexToBytes(p.dleq.s), + e: hexToBytes(p.dleq.e), + r: rs[i] + } as DLEQ); + const blindSignature = { + id: p.id, + amount: p.amount, + C_: pointFromHex(p.C_), + dleq: dleq + }; + const r = rs[i]; + const secret = secrets[i]; + const A = pointFromHex(keyset.keys[p.amount]); + const proof = constructProofFromPromise(blindSignature, r, secret, A); + if (verifyDLEQ) { + if (dleq == undefined) { + throw new Error('DLEQ verification required, but none found'); + } + if (!verifyDLEQProof_reblind(secret, dleq, proof.C, A)) { + throw new Error('DLEQ verification failed'); + } + } + return { + id: proof.id, + amount: proof.amount, + secret: bytesToHex(proof.secret), + C: proof.C.toHex(true), + dleq: + dleq == undefined + ? undefined + : { + s: bytesToHex(dleq.s), + e: bytesToHex(dleq.e), + r: dleq.r?.toString(16) + } + } as Proof; + }); } } diff --git a/src/model/BlindedSignature.ts b/src/model/BlindedSignature.ts index e998f320..85ca7f15 100644 --- a/src/model/BlindedSignature.ts +++ b/src/model/BlindedSignature.ts @@ -1,19 +1,35 @@ import { ProjPointType } from '@noble/curves/abstract/weierstrass'; import { SerializedBlindedSignature } from './types/index.js'; +import { DLEQ } from '@cashu/crypto/modules/common'; +import { bytesToHex } from '@noble/hashes/utils.js'; class BlindedSignature { id: string; amount: number; C_: ProjPointType; + dleq?: DLEQ; - constructor(id: string, amount: number, C_: ProjPointType) { + constructor(id: string, amount: number, C_: ProjPointType, dleq: DLEQ) { this.id = id; this.amount = amount; this.C_ = C_; + this.dleq = dleq; } getSerializedBlindedSignature(): SerializedBlindedSignature { - return { id: this.id, amount: this.amount, C_: this.C_.toHex(true) }; + return { + id: this.id, + amount: this.amount, + C_: this.C_.toHex(true), + dleq: + this.dleq == undefined + ? undefined + : { + s: bytesToHex(this.dleq.s), + e: bytesToHex(this.dleq.e), + r: this.dleq.r?.toString(16) + } + }; } } diff --git a/src/model/types/mint/responses.ts b/src/model/types/mint/responses.ts index 9f42b935..3de1208c 100644 --- a/src/model/types/mint/responses.ts +++ b/src/model/types/mint/responses.ts @@ -178,6 +178,16 @@ export type PostRestoreResponse = { promises: Array; }; +/* + * Zero-Knowledge that BlindedSignature + * was generated using a specific public key + */ +export type SerializedDLEQ = { + s: string; + e: string; + r?: string; +}; + /** * Blinded signature as it is received from the mint */ @@ -194,6 +204,10 @@ export type SerializedBlindedSignature = { * Blinded signature */ C_: string; + /** + * DLEQ Proof + */ + dleq?: SerializedDLEQ; }; /** diff --git a/src/model/types/wallet/index.ts b/src/model/types/wallet/index.ts index 853df5d9..c6516573 100644 --- a/src/model/types/wallet/index.ts +++ b/src/model/types/wallet/index.ts @@ -1,3 +1,5 @@ +import { SerializedDLEQ } from '../mint'; + export * from './payloads'; export * from './responses'; export * from './tokens'; @@ -23,6 +25,10 @@ export type Proof = { * The unblinded signature for this secret, signed by the mints private key. */ C: string; + /** + * DLEQ proof + */ + dleq?: SerializedDLEQ; }; /** From d794f2c583309a9553f68712a28645d5c25d822a Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 29 Oct 2024 07:54:48 +0100 Subject: [PATCH 02/27] less tab --- src/model/BlindedSignature.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/BlindedSignature.ts b/src/model/BlindedSignature.ts index 85ca7f15..3dece8ac 100644 --- a/src/model/BlindedSignature.ts +++ b/src/model/BlindedSignature.ts @@ -28,7 +28,7 @@ class BlindedSignature { s: bytesToHex(this.dleq.s), e: bytesToHex(this.dleq.e), r: this.dleq.r?.toString(16) - } + } }; } } From 40b54680463c42e39886fb655a04eaa074fd216c Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 29 Oct 2024 13:31:01 +0100 Subject: [PATCH 03/27] package.json --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index aee19c7e..c7ff8a6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.1.0-3", "license": "MIT", "dependencies": { - "@cashu/crypto": "^0.3.1", + "@cashu/crypto": "^0.3.3", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", @@ -621,9 +621,9 @@ "dev": true }, "node_modules/@cashu/crypto": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.1.tgz", - "integrity": "sha512-lxzUQgcz3lD/z2vFHgyPV5zZ/7kGdeJQDjvC5coReDD1eRRnZv1pBLEJTApGx3g0ZK3MD4ScCsLpnGJHNNq5sQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.3.tgz", + "integrity": "sha512-os/QS74FtrsY5rpnKMFsKlfUSW5g/QB2gcaHm1qYTwhKIND271qhGQGs69mbWE3iBa2s8mEQSmocy6O1J6vgHA==", "dependencies": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", @@ -6835,9 +6835,9 @@ "dev": true }, "@cashu/crypto": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.1.tgz", - "integrity": "sha512-lxzUQgcz3lD/z2vFHgyPV5zZ/7kGdeJQDjvC5coReDD1eRRnZv1pBLEJTApGx3g0ZK3MD4ScCsLpnGJHNNq5sQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.3.tgz", + "integrity": "sha512-os/QS74FtrsY5rpnKMFsKlfUSW5g/QB2gcaHm1qYTwhKIND271qhGQGs69mbWE3iBa2s8mEQSmocy6O1J6vgHA==", "requires": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", diff --git a/package.json b/package.json index 80ae4399..876755f4 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "typescript": "^5.0.4" }, "dependencies": { - "@cashu/crypto": "^0.3.1", + "@cashu/crypto": "^0.3.3", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", From c7ac6d9b245ca51326ba1c4c058ce7f50553fa1d Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 29 Oct 2024 15:10:23 +0100 Subject: [PATCH 04/27] fix --- src/CashuWallet.ts | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 83970627..ffe85fa5 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -21,7 +21,8 @@ import { GetInfoResponse, OutputAmounts, CheckStateEntry, - BlindingData + BlindingData, + SerializedDLEQ } from './model/types/index.js'; import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts } from './utils.js'; import { validateMnemonic } from '@scure/bip39'; @@ -1058,20 +1059,15 @@ class CashuWallet { throw new Error('DLEQ verification failed'); } } - return { - id: proof.id, - amount: proof.amount, - secret: bytesToHex(proof.secret), - C: proof.C.toHex(true), - dleq: - dleq == undefined - ? undefined - : { - s: bytesToHex(dleq.s), - e: bytesToHex(dleq.e), - r: dleq.r?.toString(16) - } - } as Proof; + const serializedProof = serializeProof(proof) as Proof; + serializedProof.dleq = dleq == undefined + ? undefined + : { + s: bytesToHex(dleq.s), + e: bytesToHex(dleq.e), + r: dleq.r?.toString(16) + } as SerializedDLEQ; + return serializedProof; }); } } From 102908f727dede701cf46129c50ab920f0419666 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 30 Oct 2024 18:06:22 +0100 Subject: [PATCH 05/27] add optional `dleqValid` to `Proof` type. It will be true if the mint returned a DLEQ and it was verified correctly. --- src/CashuWallet.ts | 37 ++++++--------------------------- src/model/types/wallet/index.ts | 4 ++++ 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 5bfee8c3..54e4fbd6 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -275,13 +275,11 @@ class CashuWallet { options?.privkey ); const { signatures } = await this.mint.swap(payload); - const requireDleq = options?.requireDLEQ; const freshProofs = this.constructProofs( signatures, blindingData.blindingFactors, blindingData.secrets, - keys, - requireDleq ?? false + keys ); return freshProofs; } @@ -463,7 +461,6 @@ class CashuWallet { * @param options.proofsWeHave? optionally provide all currently stored proofs of this mint. Cashu-ts will use them to derive the optimal output amounts * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param options.privkey? will create a signature on the @param proofs secrets if set - * @param options.requireDLEQ? optionally require a DLEQ proof from the mint. * @returns promise of the change- and send-proofs */ async swap( @@ -477,7 +474,6 @@ class CashuWallet { privkey?: string; keysetId?: string; includeFees?: boolean; - requireDLEQ?: boolean; } ): Promise { if (!options) options = {}; @@ -550,13 +546,11 @@ class CashuWallet { options?.privkey ); const { signatures } = await this.mint.swap(payload); - const requireDleq = options?.requireDLEQ; const swapProofs = this.constructProofs( signatures, blindingData.blindingFactors, blindingData.secrets, - keyset, - requireDleq ?? false + keyset ); const splitProofsToKeep: Array = []; const splitProofsToSend: Array = []; @@ -580,15 +574,12 @@ class CashuWallet { * @param start set starting point for count (first cycle for each keyset should usually be 0) * @param count set number of blinded messages that should be generated * @param options.keysetId set a custom keysetId to restore from. keysetIds can be loaded with `CashuMint.getKeySets()` - * @param options.requireDLEQ require a DLEQ proof - * @returns proofs */ async restore( start: number, count: number, options?: { keysetId?: string; - requireDLEQ?: boolean; } ): Promise<{ proofs: Array }> { const keys = await this.getKeys(options?.keysetId); @@ -612,14 +603,12 @@ class CashuWallet { const validSecrets = secrets.filter((_: Uint8Array, i: number) => outputs.map((o: SerializedBlindedMessage) => o.B_).includes(blindedMessages[i].B_) ); - const requireDleq = options?.requireDLEQ; return { proofs: this.constructProofs( promises, validBlindingFactors, validSecrets, keys, - requireDleq ?? false ) }; } @@ -657,7 +646,6 @@ class CashuWallet { * @param options.outputAmounts? optionally specify the output's amounts to keep and to send. * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! - * @param options.requireDLEQ? optionally require a DLEQ proof. * @returns proofs */ async mintProofs( @@ -669,7 +657,6 @@ class CashuWallet { proofsWeHave?: Array; counter?: number; pubkey?: string; - requireDLEQ?: boolean; } ): Promise<{ proofs: Array }> { const keyset = await this.getKeys(options?.keysetId); @@ -697,14 +684,12 @@ class CashuWallet { quote: quote }; const { signatures } = await this.mint.mint(mintPayload); - const requireDleq = options?.requireDLEQ; return { proofs: this.constructProofs( signatures, blindingFactors, secrets, keyset, - requireDleq ?? false ) }; } @@ -750,7 +735,6 @@ class CashuWallet { keysetId?: string; counter?: number; privkey?: string; - requireDLEQ?: boolean; } ): Promise { const keys = await this.getKeys(options?.keysetId); @@ -779,14 +763,12 @@ class CashuWallet { }; const meltResponse = await this.mint.melt(meltPayload); let change: Array = []; - const requireDleq = options?.requireDLEQ; if (meltResponse.change) { change = this.constructProofs( meltResponse.change, blindingFactors, secrets, keys, - requireDleq ?? false ); } return { @@ -1001,15 +983,13 @@ class CashuWallet { * @param rs arrays of binding factors * @param secrets array of secrets * @param keyset mint keyset - * @param verifyDLEQ require proof of same secret (DLEQ) * @returns array of serialized proofs */ private constructProofs( promises: Array, rs: Array, secrets: Array, - keyset: MintKeys, - verifyDLEQ: boolean + keyset: MintKeys ): Array { return promises.map((p: SerializedBlindedSignature, i: number) => { const dleq = @@ -1030,15 +1010,10 @@ class CashuWallet { const secret = secrets[i]; const A = pointFromHex(keyset.keys[p.amount]); const proof = constructProofFromPromise(blindSignature, r, secret, A); - if (verifyDLEQ) { - if (dleq == undefined) { - throw new Error('DLEQ verification required, but none found'); - } - if (!verifyDLEQProof_reblind(secret, dleq, proof.C, A)) { - throw new Error('DLEQ verification failed'); - } - } const serializedProof = serializeProof(proof) as Proof; + serializedProof.dleqValid = dleq == undefined + ? undefined + : verifyDLEQProof_reblind(secret, dleq, proof.C, A); serializedProof.dleq = dleq == undefined ? undefined : { diff --git a/src/model/types/wallet/index.ts b/src/model/types/wallet/index.ts index c6516573..641eac64 100644 --- a/src/model/types/wallet/index.ts +++ b/src/model/types/wallet/index.ts @@ -29,6 +29,10 @@ export type Proof = { * DLEQ proof */ dleq?: SerializedDLEQ; + /** + * Is the associated DLEQ proof valid? + */ + dleqValid?: boolean; }; /** From e4a59f6a2cc458dfde961bc4d4b85c920df0da7a Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 30 Oct 2024 20:15:48 +0100 Subject: [PATCH 06/27] include dleq in encoded V4 token if provided --- src/model/types/wallet/tokens.ts | 19 +++++++++++++++++++ src/utils.ts | 24 ++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/model/types/wallet/tokens.ts b/src/model/types/wallet/tokens.ts index 8d32662b..bd0c9550 100644 --- a/src/model/types/wallet/tokens.ts +++ b/src/model/types/wallet/tokens.ts @@ -22,6 +22,21 @@ export type Token = { unit?: string; }; +export type V4DLEQTemplate = { + /** + * challenge + */ + e: Uint8Array; + /** + * response + */ + s: Uint8Array; + /** + * blinding factor + */ + r: Uint8Array; +} + /** * Template for a Proof inside a V4 Token */ @@ -38,6 +53,10 @@ export type V4ProofTemplate = { * Signature */ c: Uint8Array; + /** + * DLEQ + */ + d?: V4DLEQTemplate; }; /** diff --git a/src/utils.ts b/src/utils.ts index 4983db88..3210f39b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -10,6 +10,7 @@ import { Proof, Token, TokenV4Template, + V4DLEQTemplate, V4InnerToken, V4ProofTemplate } from './model/types/index.js'; @@ -181,7 +182,15 @@ export function getEncodedToken(token: Token): string { * @param token to encode * @returns encoded token */ -export function getEncodedTokenV4(token: Token): string { +export function getEncodedTokenV4( + token: Token, +): string { + // Make sure each DLEQ has its blinding factor + token.proofs.forEach(p => { + if (p.dleq && p.dleq.r == undefined) { + throw new Error("Missing blinding factor in included DLEQ proof"); + } + }); const idMap: { [id: string]: Array } = {}; const mint = token.mint; for (let i = 0; i < token.proofs.length; i++) { @@ -199,7 +208,18 @@ export function getEncodedTokenV4(token: Token): string { (id: string): V4InnerToken => ({ i: hexToBytes(id), p: idMap[id].map( - (p: Proof): V4ProofTemplate => ({ a: p.amount, s: p.secret, c: hexToBytes(p.C) }) + (p: Proof): V4ProofTemplate => ({ + a: p.amount, + s: p.secret, + c: hexToBytes(p.C), + d: p.dleq == undefined + ? undefined + : { + e: hexToBytes(p.dleq.e), + s: hexToBytes(p.dleq.s), + r: hexToBytes(p.dleq.r ?? "00"), + } as V4DLEQTemplate + }) ) }) ) From 72f4211f98d2ca4f4d5c9db312aa74d76ee18332 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 1 Nov 2024 14:10:22 +0100 Subject: [PATCH 07/27] handleTokens include dleq if present. --- src/utils.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 3210f39b..f1f36e25 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,6 +8,7 @@ import { DeprecatedToken, Keys, Proof, + SerializedDLEQ, Token, TokenV4Template, V4DLEQTemplate, @@ -286,7 +287,14 @@ export function handleTokens(token: string): Token { secret: p.s, C: bytesToHex(p.c), amount: p.a, - id: bytesToHex(t.i) + id: bytesToHex(t.i), + dleq: p.d == undefined + ? undefined + : { + e: bytesToHex(p.d.e), + s: bytesToHex(p.d.s), + r: bytesToHex(p.d.r) + } as SerializedDLEQ, }); }) ); From e7c6501ac136409ca659801178a3e6fa43af6a03 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 1 Nov 2024 14:37:01 +0100 Subject: [PATCH 08/27] fix tests accordingly --- test/utils.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/utils.test.ts b/test/utils.test.ts index bff0d6bf..127ce63f 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -175,7 +175,8 @@ describe('test decode token', () => { secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', id: '00ad268c4d1f5826', - amount: 1 + amount: 1, + dleq: undefined, } ] }; @@ -195,19 +196,22 @@ describe('test decode token', () => { secret: 'acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388', C: '0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf', id: '00ffd48b8f5ecf80', - amount: 1 + amount: 1, + dleq: undefined, }, { secret: '1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee', C: '023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d', id: '00ad268c4d1f5826', - amount: 2 + amount: 2, + dleq: undefined, }, { secret: '56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57', C: '0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63', id: '00ad268c4d1f5826', - amount: 1 + amount: 1, + dleq: undefined, } ] }; @@ -240,7 +244,8 @@ describe('test v4 encoding', () => { secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', id: '00ad268c4d1f5826', - amount: 1 + amount: 1, + dleq: undefined, } ], unit: 'sat' From 2379ed532e285f3bf7a7a9001e69baeadea7caaf Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 1 Nov 2024 15:59:27 +0100 Subject: [PATCH 09/27] Carol receive from Alice --- src/CashuWallet.ts | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 54e4fbd6..83af64a1 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -22,7 +22,7 @@ import { BlindingData, SerializedDLEQ } from './model/types/index.js'; -import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts } from './utils.js'; +import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts, hexToNumber } from './utils.js'; import { validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; @@ -247,6 +247,7 @@ class CashuWallet { * @param options.counter? optionally set counter to derive secret deterministically. CashuWallet class must be initialized with seed phrase to take effect * @param options.pubkey? optionally locks ecash to pubkey. Will not be deterministic, even if counter is set! * @param options.privkey? will create a signature on the @param token secrets if set + * @param options.requireDleq? will check each proof for DLEQ proofs. Reject the token if any one of them can't be verified. * @returns New token with newly created proofs, token entries that had errors */ async receive( @@ -258,12 +259,16 @@ class CashuWallet { counter?: number; pubkey?: string; privkey?: string; + requireDleq?: boolean; } ): Promise> { if (typeof token === 'string') { token = getDecodedToken(token); } const keys = await this.getKeys(options?.keysetId); + if (options?.requireDleq) { + this.requireDLEQ(token, keys); + } const amount = sumProofs(token.proofs) - this.getFeesForProofs(token.proofs); const { payload, blindingData } = this.createSwapPayload( amount, @@ -1024,6 +1029,34 @@ class CashuWallet { return serializedProof; }); } + + /** + * Checks that each proof in `token` has a valid DLEQ proof according to + * keyset `keys` + * @param token The token subject to the verification + * @param keys The Mint's keyset to be used for verification + */ + private requireDLEQ(token: Token, keys: MintKeys) { + token.proofs.forEach((p: Proof, i: number) => { + if (p.dleq == undefined) { + throw new Error(`${i}-th proof is missing DLEQ proof`); + } + const dleq = { + e: hexToBytes(p.dleq.e), + s: hexToBytes(p.dleq.s), + r: hexToNumber(p.dleq.r ?? "00"), + } as DLEQ; + const key = keys.keys[p.amount]; + if (!verifyDLEQProof_reblind( + new TextEncoder().encode(p.secret), + dleq, + pointFromHex(p.C), + pointFromHex(key) + )) { + throw new Error(`${i}-th DLEQ proof is invalid for key ${key}`); + } + }); + } } export { CashuWallet }; From 36a643e9ebaf31fb1736fe976079e03471a5e56b Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 1 Nov 2024 16:20:43 +0100 Subject: [PATCH 10/27] strip DLEQs when melting, swapping --- src/CashuWallet.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 83af64a1..6b3fd22e 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -761,6 +761,11 @@ class CashuWallet { options.privkey ).map((p: NUT11Proof) => serializeProof(p)); } + // Strip DLEQs if any + proofsToSend.map((p: Proof) => { + p.dleq = undefined; + return p; + }); const meltPayload: MeltPayload = { quote: meltQuote.quote, inputs: proofsToSend, @@ -841,6 +846,12 @@ class CashuWallet { ).map((p: NUT11Proof) => serializeProof(p)); } + // Strip DLEQs if any + proofsToSend.map((p: Proof) => { + p.dleq = undefined; + return p; + }); + // join keepBlindedMessages and sendBlindedMessages const blindingData: BlindingData = { blindedMessages: [ From 87686d71acf3de9feb4c5036aff21776ba04be73 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 1 Nov 2024 16:36:00 +0100 Subject: [PATCH 11/27] npm format --- src/CashuWallet.ts | 68 +++++++++++++++----------------- src/model/BlindedSignature.ts | 2 +- src/model/types/wallet/tokens.ts | 2 +- src/utils.ts | 38 +++++++++--------- test/utils.test.ts | 10 ++--- 5 files changed, 57 insertions(+), 63 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 6b3fd22e..f1fd943f 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -22,7 +22,14 @@ import { BlindingData, SerializedDLEQ } from './model/types/index.js'; -import { bytesToNumber, getDecodedToken, splitAmount, sumProofs, getKeepAmounts, hexToNumber } from './utils.js'; +import { + bytesToNumber, + getDecodedToken, + splitAmount, + sumProofs, + getKeepAmounts, + hexToNumber +} from './utils.js'; import { validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; @@ -609,12 +616,7 @@ class CashuWallet { outputs.map((o: SerializedBlindedMessage) => o.B_).includes(blindedMessages[i].B_) ); return { - proofs: this.constructProofs( - promises, - validBlindingFactors, - validSecrets, - keys, - ) + proofs: this.constructProofs(promises, validBlindingFactors, validSecrets, keys) }; } @@ -690,12 +692,7 @@ class CashuWallet { }; const { signatures } = await this.mint.mint(mintPayload); return { - proofs: this.constructProofs( - signatures, - blindingFactors, - secrets, - keyset, - ) + proofs: this.constructProofs(signatures, blindingFactors, secrets, keyset) }; } @@ -774,12 +771,7 @@ class CashuWallet { const meltResponse = await this.mint.melt(meltPayload); let change: Array = []; if (meltResponse.change) { - change = this.constructProofs( - meltResponse.change, - blindingFactors, - secrets, - keys, - ); + change = this.constructProofs(meltResponse.change, blindingFactors, secrets, keys); } return { quote: meltResponse, @@ -1015,7 +1007,7 @@ class CashuWallet { s: hexToBytes(p.dleq.s), e: hexToBytes(p.dleq.e), r: rs[i] - } as DLEQ); + } as DLEQ); const blindSignature = { id: p.id, amount: p.amount, @@ -1027,16 +1019,16 @@ class CashuWallet { const A = pointFromHex(keyset.keys[p.amount]); const proof = constructProofFromPromise(blindSignature, r, secret, A); const serializedProof = serializeProof(proof) as Proof; - serializedProof.dleqValid = dleq == undefined - ? undefined - : verifyDLEQProof_reblind(secret, dleq, proof.C, A); - serializedProof.dleq = dleq == undefined - ? undefined - : { - s: bytesToHex(dleq.s), - e: bytesToHex(dleq.e), - r: dleq.r?.toString(16) - } as SerializedDLEQ; + serializedProof.dleqValid = + dleq == undefined ? undefined : verifyDLEQProof_reblind(secret, dleq, proof.C, A); + serializedProof.dleq = + dleq == undefined + ? undefined + : ({ + s: bytesToHex(dleq.s), + e: bytesToHex(dleq.e), + r: dleq.r?.toString(16) + } as SerializedDLEQ); return serializedProof; }); } @@ -1055,15 +1047,17 @@ class CashuWallet { const dleq = { e: hexToBytes(p.dleq.e), s: hexToBytes(p.dleq.s), - r: hexToNumber(p.dleq.r ?? "00"), + r: hexToNumber(p.dleq.r ?? '00') } as DLEQ; const key = keys.keys[p.amount]; - if (!verifyDLEQProof_reblind( - new TextEncoder().encode(p.secret), - dleq, - pointFromHex(p.C), - pointFromHex(key) - )) { + if ( + !verifyDLEQProof_reblind( + new TextEncoder().encode(p.secret), + dleq, + pointFromHex(p.C), + pointFromHex(key) + ) + ) { throw new Error(`${i}-th DLEQ proof is invalid for key ${key}`); } }); diff --git a/src/model/BlindedSignature.ts b/src/model/BlindedSignature.ts index 3dece8ac..85ca7f15 100644 --- a/src/model/BlindedSignature.ts +++ b/src/model/BlindedSignature.ts @@ -28,7 +28,7 @@ class BlindedSignature { s: bytesToHex(this.dleq.s), e: bytesToHex(this.dleq.e), r: this.dleq.r?.toString(16) - } + } }; } } diff --git a/src/model/types/wallet/tokens.ts b/src/model/types/wallet/tokens.ts index bd0c9550..011a8b4f 100644 --- a/src/model/types/wallet/tokens.ts +++ b/src/model/types/wallet/tokens.ts @@ -35,7 +35,7 @@ export type V4DLEQTemplate = { * blinding factor */ r: Uint8Array; -} +}; /** * Template for a Proof inside a V4 Token diff --git a/src/utils.ts b/src/utils.ts index f1f36e25..4c1c5a1a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -183,13 +183,11 @@ export function getEncodedToken(token: Token): string { * @param token to encode * @returns encoded token */ -export function getEncodedTokenV4( - token: Token, -): string { +export function getEncodedTokenV4(token: Token): string { // Make sure each DLEQ has its blinding factor - token.proofs.forEach(p => { + token.proofs.forEach((p) => { if (p.dleq && p.dleq.r == undefined) { - throw new Error("Missing blinding factor in included DLEQ proof"); + throw new Error('Missing blinding factor in included DLEQ proof'); } }); const idMap: { [id: string]: Array } = {}; @@ -213,13 +211,14 @@ export function getEncodedTokenV4( a: p.amount, s: p.secret, c: hexToBytes(p.C), - d: p.dleq == undefined - ? undefined - : { - e: hexToBytes(p.dleq.e), - s: hexToBytes(p.dleq.s), - r: hexToBytes(p.dleq.r ?? "00"), - } as V4DLEQTemplate + d: + p.dleq == undefined + ? undefined + : ({ + e: hexToBytes(p.dleq.e), + s: hexToBytes(p.dleq.s), + r: hexToBytes(p.dleq.r ?? '00') + } as V4DLEQTemplate) }) ) }) @@ -288,13 +287,14 @@ export function handleTokens(token: string): Token { C: bytesToHex(p.c), amount: p.a, id: bytesToHex(t.i), - dleq: p.d == undefined - ? undefined - : { - e: bytesToHex(p.d.e), - s: bytesToHex(p.d.s), - r: bytesToHex(p.d.r) - } as SerializedDLEQ, + dleq: + p.d == undefined + ? undefined + : ({ + e: bytesToHex(p.d.e), + s: bytesToHex(p.d.s), + r: bytesToHex(p.d.r) + } as SerializedDLEQ) }); }) ); diff --git a/test/utils.test.ts b/test/utils.test.ts index 127ce63f..7618bc73 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -176,7 +176,7 @@ describe('test decode token', () => { C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', id: '00ad268c4d1f5826', amount: 1, - dleq: undefined, + dleq: undefined } ] }; @@ -197,21 +197,21 @@ describe('test decode token', () => { C: '0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf', id: '00ffd48b8f5ecf80', amount: 1, - dleq: undefined, + dleq: undefined }, { secret: '1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee', C: '023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d', id: '00ad268c4d1f5826', amount: 2, - dleq: undefined, + dleq: undefined }, { secret: '56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57', C: '0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63', id: '00ad268c4d1f5826', amount: 1, - dleq: undefined, + dleq: undefined } ] }; @@ -245,7 +245,7 @@ describe('test v4 encoding', () => { C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', id: '00ad268c4d1f5826', amount: 1, - dleq: undefined, + dleq: undefined } ], unit: 'sat' From f8e66e73f5087155f3c970d5415e9c538fa99855 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 5 Nov 2024 17:02:04 +0100 Subject: [PATCH 12/27] remove unused imports + update cashu/crypto dependency to latest release. --- package-lock.json | 14 +++++++------- package.json | 2 +- src/CashuWallet.ts | 2 -- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f6d05b6..67df9f20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.0.0-rc1", "license": "MIT", "dependencies": { - "@cashu/crypto": "^0.3.3", + "@cashu/crypto": "^0.3.4", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", @@ -620,9 +620,9 @@ "dev": true }, "node_modules/@cashu/crypto": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.3.tgz", - "integrity": "sha512-os/QS74FtrsY5rpnKMFsKlfUSW5g/QB2gcaHm1qYTwhKIND271qhGQGs69mbWE3iBa2s8mEQSmocy6O1J6vgHA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.4.tgz", + "integrity": "sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA==", "dependencies": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", @@ -6834,9 +6834,9 @@ "dev": true }, "@cashu/crypto": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.3.tgz", - "integrity": "sha512-os/QS74FtrsY5rpnKMFsKlfUSW5g/QB2gcaHm1qYTwhKIND271qhGQGs69mbWE3iBa2s8mEQSmocy6O1J6vgHA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.4.tgz", + "integrity": "sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA==", "requires": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", diff --git a/package.json b/package.json index 613c6a1b..747cb8fb 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "typescript": "^5.0.4" }, "dependencies": { - "@cashu/crypto": "^0.3.3", + "@cashu/crypto": "^0.3.4", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@scure/bip32": "^1.3.3", diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index bfe9df36..fae3211c 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -30,8 +30,6 @@ import { getKeepAmounts, hexToNumber } from './utils.js'; -import { validateMnemonic } from '@scure/bip39'; -import { wordlist } from '@scure/bip39/wordlists/english'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; import { blindMessage, From 6b8c7c73f0ad07cec43ad8bb40db673b39c96c50 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 6 Nov 2024 15:51:12 +0100 Subject: [PATCH 13/27] integration tests for verify and includeDleq in token + some fixes --- src/CashuWallet.ts | 35 ++++++++++++++++++++++++++------- test/integration.test.ts | 42 ++++++++++++++++++++++++++++++++++++++-- test/wallet.test.ts | 7 +++++++ 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index fae3211c..886f391c 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -299,6 +299,7 @@ class CashuWallet { * @param options.keysetId? override the keysetId derived from the current mintKeys with a custom one. This should be a keyset that was fetched from the `/keysets` endpoint * @param options.offline? optionally send proofs offline. * @param options.includeFees? optionally include fees in the response. + * @param options.includeDleq? optionally include DLEQ proof in the proofs to send. * @returns {SendResponse} */ async send( @@ -313,6 +314,7 @@ class CashuWallet { keysetId?: string; offline?: boolean; includeFees?: boolean; + includeDleq?: boolean; } ): Promise { if (sumProofs(proofs) < amount) { @@ -321,7 +323,8 @@ class CashuWallet { const { keep: keepProofsOffline, send: sendProofOffline } = this.selectProofsToSend( proofs, amount, - options?.includeFees + options?.includeFees, + options?.includeDleq, ); const expectedFee = options?.includeFees ? this.getFeesForProofs(sendProofOffline) : 0; if ( @@ -337,7 +340,8 @@ class CashuWallet { const { keep: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( proofs, amount, - true + true, + options?.includeDleq, ); options?.proofsWeHave?.push(...keepProofsSelect); @@ -356,8 +360,13 @@ class CashuWallet { selectProofsToSend( proofs: Array, amountToSend: number, - includeFees?: boolean + includeFees?: boolean, + includeDleq?: boolean, ): SendResponse { + if (includeDleq ?? false) { + // only pick the ones with a DLEQ proof + proofs = proofs.filter((p: Proof) => p.dleq != undefined); + } const sortedProofs = proofs.sort((a: Proof, b: Proof) => a.amount - b.amount); const smallerProofs = sortedProofs .filter((p: Proof) => p.amount <= amountToSend) @@ -386,7 +395,8 @@ class CashuWallet { const { keep, send } = this.selectProofsToSend( smallerProofs.slice(1), remainder, - includeFees + includeFees, + includeDleq, ); selectedProofs.push(...send); returnedProofs.push(...keep); @@ -396,8 +406,19 @@ class CashuWallet { if (sumProofs(selectedProofs) < amountToSend + selectedFeePPK && nextBigger) { selectedProofs = [nextBigger]; } + + const keepProofs = proofs.filter((p: Proof) => !selectedProofs.includes(p)); + + // if explicitly told to, strip DLEQ + if (!includeDleq) { + selectedProofs = selectedProofs.map((p: Proof) => { + p.dleq = undefined; + return p; + }); + } + return { - keep: proofs.filter((p: Proof) => !selectedProofs.includes(p)), + keep: keepProofs, send: selectedProofs }; } @@ -750,7 +771,7 @@ class CashuWallet { ).map((p: NUT11Proof) => serializeProof(p)); } // Strip DLEQs if any - proofsToSend.map((p: Proof) => { + proofsToSend = proofsToSend.map((p: Proof) => { p.dleq = undefined; return p; }); @@ -830,7 +851,7 @@ class CashuWallet { } // Strip DLEQs if any - proofsToSend.map((p: Proof) => { + proofsToSend = proofsToSend.map((p: Proof) => { p.dleq = undefined; return p; }); diff --git a/test/integration.test.ts b/test/integration.test.ts index d6528d82..8ff2591d 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -2,10 +2,10 @@ import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; import dns from 'node:dns'; -import { deriveKeysetId, getEncodedToken, sumProofs } from '../src/utils.js'; +import { deriveKeysetId, getEncodedToken, getEncodedTokenV4, sumProofs } from '../src/utils.js'; import { secp256k1 } from '@noble/curves/secp256k1'; import { bytesToHex } from '@noble/curves/abstract/utils'; -import { CheckStateEnum, MeltQuoteState } from '../src/model/types/index.js'; +import { CheckStateEnum, MeltQuoteState, Token } from '../src/model/types/index.js'; dns.setDefaultResultOrder('ipv4first'); const externalInvoice = @@ -253,4 +253,42 @@ describe('mint api', () => { expect(response).toBeDefined(); expect(response.quote.state == MeltQuoteState.PAID).toBe(true); }); + test('mint and check dleq', async () => { + const mint = new CashuMint(mintUrl); + const NUT12 = (await mint.getInfo()).nuts['12']; + if (NUT12 == undefined || !NUT12.supported) { + throw new Error("Cannot run this test: mint does not support NUT12"); + } + const wallet = new CashuWallet(mint); + + const mintRequest = await wallet.createMintQuote(3000); + const { proofs } = await wallet.mintProofs(3000, mintRequest.quote); + + proofs.forEach(p => { + expect(p).toHaveProperty('dleq'); + expect(p.dleq).toHaveProperty('s'); + expect(p.dleq).toHaveProperty('e'); + expect(p.dleq).toHaveProperty('r'); + expect(p).toHaveProperty('dleqValid', true); + }); + }); + test('send and receive token with dleq', async () => { + const mint = new CashuMint(mintUrl); + const wallet = new CashuWallet(mint); + + const mintRequest = await wallet.createMintQuote(3000); + const { proofs } = await wallet.mintProofs(3000, mintRequest.quote); + + const { keep, send } = await wallet.send(1500, proofs, { includeDleq: true }); + + send.forEach(p => {expect(p.dleq).toBeDefined(); expect(p.dleq?.r).toBeDefined()}); + const token = { + mint: mint.mintUrl, + proofs: send + } as Token; + const encodedToken = getEncodedTokenV4(token); + const newProofs = await wallet.receive(encodedToken, { requireDleq: true }) + console.log(getEncodedTokenV4(token)); + expect(newProofs).toBeDefined(); + }); }); diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 7ca5c27b..129dbf04 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -199,6 +199,13 @@ describe('receive', () => { const result = await wallet.receive(tokenInput).catch((e) => e); expect(result).toEqual(new Error('could not verify proofs.')); }); + + /* + test('test receive with dleq', async() => { + nock(mintUrl).post('/v1/swap').reply(200, {}); + const wallet = new CashuWallet(mint, { unit }); + }) + */ }); describe('checkProofsStates', () => { From 8c12a87fdcf5e0392f37ef26f4493895e25877e3 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 6 Nov 2024 18:21:20 +0100 Subject: [PATCH 14/27] sure-fire pad to 64 characters for bigint --- src/CashuWallet.ts | 6 +++--- src/utils.ts | 9 +++++++++ test/integration.test.ts | 6 +++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 886f391c..e4faa55f 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -28,7 +28,8 @@ import { splitAmount, sumProofs, getKeepAmounts, - hexToNumber + hexToNumber, + numberToHexPadded64 } from './utils.js'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; import { @@ -40,7 +41,6 @@ import { deriveBlindingFactor, deriveSecret } from '@cashu/crypto/modules/client import { createP2PKsecret, getSignedProofs } from '@cashu/crypto/modules/client/NUT11'; import { type Proof as NUT11Proof, DLEQ } from '@cashu/crypto/modules/common/index'; import { verifyDLEQProof_reblind } from '@cashu/crypto/modules/client/NUT12'; - /** * The default number of proofs per denomination to keep in a wallet. */ @@ -1039,7 +1039,7 @@ class CashuWallet { : ({ s: bytesToHex(dleq.s), e: bytesToHex(dleq.e), - r: dleq.r?.toString(16) + r: numberToHexPadded64(dleq.r ?? BigInt(0)), } as SerializedDLEQ); return serializedProof; }); diff --git a/src/utils.ts b/src/utils.ts index fb7f1afe..471e7d28 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -152,6 +152,15 @@ export function hexToNumber(hex: string): bigint { return BigInt(`0x${hex}`); } +/** + * + * @param number (bigint) to conver to hex + * @returns hex string start-padded to 64 characters + */ +export function numberToHexPadded64(number: bigint): string { + return number.toString(16).padStart(64, '0'); +} + function isValidHex(str: string) { return /^[a-f0-9]*$/i.test(str); } diff --git a/test/integration.test.ts b/test/integration.test.ts index 8ff2591d..f3547082 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -281,7 +281,11 @@ describe('mint api', () => { const { keep, send } = await wallet.send(1500, proofs, { includeDleq: true }); - send.forEach(p => {expect(p.dleq).toBeDefined(); expect(p.dleq?.r).toBeDefined()}); + send.forEach(p => { + expect(p.dleq).toBeDefined(); + expect(p.dleq?.r).toBeDefined(); + }); + const token = { mint: mint.mintUrl, proofs: send From 5cb0d078951e88d524b93f75cf1bae5b7e3cc51b Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 6 Nov 2024 19:33:43 +0100 Subject: [PATCH 15/27] check pubkey is not undefined --- src/CashuWallet.ts | 3 +++ test/wallet.test.ts | 7 ------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index e4faa55f..87108882 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -1062,6 +1062,9 @@ class CashuWallet { r: hexToNumber(p.dleq.r ?? '00') } as DLEQ; const key = keys.keys[p.amount]; + if (key == undefined) { + throw new Error(`undefined key for amount ${p.amount}`); + } if ( !verifyDLEQProof_reblind( new TextEncoder().encode(p.secret), diff --git a/test/wallet.test.ts b/test/wallet.test.ts index 129dbf04..7ca5c27b 100644 --- a/test/wallet.test.ts +++ b/test/wallet.test.ts @@ -199,13 +199,6 @@ describe('receive', () => { const result = await wallet.receive(tokenInput).catch((e) => e); expect(result).toEqual(new Error('could not verify proofs.')); }); - - /* - test('test receive with dleq', async() => { - nock(mintUrl).post('/v1/swap').reply(200, {}); - const wallet = new CashuWallet(mint, { unit }); - }) - */ }); describe('checkProofsStates', () => { From 8aea25fd3e7782f41b1eb55d01baabc6febfeee7 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 7 Nov 2024 08:37:09 +0100 Subject: [PATCH 16/27] fix dleq strip --- src/CashuWallet.ts | 57 ++++++++++++++++++++-------------------- test/integration.test.ts | 32 +++++++++++++++++++--- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 87108882..6f117495 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -317,14 +317,17 @@ class CashuWallet { includeDleq?: boolean; } ): Promise { + if (options?.includeDleq ?? false) { + // only pick the ones with a DLEQ proof + proofs = proofs.filter((p: Proof) => p.dleq != undefined); + } if (sumProofs(proofs) < amount) { throw new Error('Not enough funds available to send'); } const { keep: keepProofsOffline, send: sendProofOffline } = this.selectProofsToSend( proofs, amount, - options?.includeFees, - options?.includeDleq, + options?.includeFees ); const expectedFee = options?.includeFees ? this.getFeesForProofs(sendProofOffline) : 0; if ( @@ -340,33 +343,42 @@ class CashuWallet { const { keep: keepProofsSelect, send: sendProofs } = this.selectProofsToSend( proofs, amount, - true, - options?.includeDleq, + true ); options?.proofsWeHave?.push(...keepProofsSelect); - const { keep, send } = await this.swap(amount, sendProofs, options); - const keepProofs = keepProofsSelect.concat(keep); - return { keep: keepProofs, send }; + let { keep, send } = await this.swap(amount, sendProofs, options); + keep = keepProofsSelect.concat(keep); + + // strip dleq if explicitly told so + if (!options?.includeDleq) { + send = send.map((p: Proof) => { + return {...p, dleq: undefined }; + }); + } + + return { keep, send }; } if (sumProofs(sendProofOffline) < amount + expectedFee) { throw new Error('Not enough funds available to send'); } + // strip dleq if explicitly told so + if (!options?.includeDleq) { + sendProofOffline.forEach((p: Proof) => { + p.dleq = undefined; + }); + } + return { keep: keepProofsOffline, send: sendProofOffline }; } selectProofsToSend( proofs: Array, amountToSend: number, - includeFees?: boolean, - includeDleq?: boolean, + includeFees?: boolean ): SendResponse { - if (includeDleq ?? false) { - // only pick the ones with a DLEQ proof - proofs = proofs.filter((p: Proof) => p.dleq != undefined); - } const sortedProofs = proofs.sort((a: Proof, b: Proof) => a.amount - b.amount); const smallerProofs = sortedProofs .filter((p: Proof) => p.amount <= amountToSend) @@ -396,7 +408,6 @@ class CashuWallet { smallerProofs.slice(1), remainder, includeFees, - includeDleq, ); selectedProofs.push(...send); returnedProofs.push(...keep); @@ -406,19 +417,9 @@ class CashuWallet { if (sumProofs(selectedProofs) < amountToSend + selectedFeePPK && nextBigger) { selectedProofs = [nextBigger]; } - - const keepProofs = proofs.filter((p: Proof) => !selectedProofs.includes(p)); - - // if explicitly told to, strip DLEQ - if (!includeDleq) { - selectedProofs = selectedProofs.map((p: Proof) => { - p.dleq = undefined; - return p; - }); - } return { - keep: keepProofs, + keep: proofs.filter((p: Proof) => !selectedProofs.includes(p)), send: selectedProofs }; } @@ -772,8 +773,7 @@ class CashuWallet { } // Strip DLEQs if any proofsToSend = proofsToSend.map((p: Proof) => { - p.dleq = undefined; - return p; + return { ...p, dleq: undefined }; }); const meltPayload: MeltPayload = { quote: meltQuote.quote, @@ -852,8 +852,7 @@ class CashuWallet { // Strip DLEQs if any proofsToSend = proofsToSend.map((p: Proof) => { - p.dleq = undefined; - return p; + return { ...p, dleq: undefined }; }); // join keepBlindedMessages and sendBlindedMessages diff --git a/test/integration.test.ts b/test/integration.test.ts index f3547082..6353ef54 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -253,6 +253,8 @@ describe('mint api', () => { expect(response).toBeDefined(); expect(response.quote.state == MeltQuoteState.PAID).toBe(true); }); +}); +describe('dleq', () => { test('mint and check dleq', async () => { const mint = new CashuMint(mintUrl); const NUT12 = (await mint.getInfo()).nuts['12']; @@ -275,11 +277,15 @@ describe('mint api', () => { test('send and receive token with dleq', async () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint); + const NUT12 = (await mint.getInfo()).nuts['12']; + if (NUT12 == undefined || !NUT12.supported) { + throw new Error("Cannot run this test: mint does not support NUT12"); + } - const mintRequest = await wallet.createMintQuote(3000); - const { proofs } = await wallet.mintProofs(3000, mintRequest.quote); + const mintRequest = await wallet.createMintQuote(8); + const { proofs } = await wallet.mintProofs(8, mintRequest.quote); - const { keep, send } = await wallet.send(1500, proofs, { includeDleq: true }); + const { keep, send } = await wallet.send(4, proofs, { includeDleq: true }); send.forEach(p => { expect(p.dleq).toBeDefined(); @@ -295,4 +301,24 @@ describe('mint api', () => { console.log(getEncodedTokenV4(token)); expect(newProofs).toBeDefined(); }); + test('send strip dleq', async() => { + const mint = new CashuMint(mintUrl); + const wallet = new CashuWallet(mint); + const NUT12 = (await mint.getInfo()).nuts['12']; + if (NUT12 == undefined || !NUT12.supported) { + throw new Error("Cannot run this test: mint does not support NUT12"); + } + + const mintRequest = await wallet.createMintQuote(8); + const { proofs } = await wallet.mintProofs(8, mintRequest.quote); + + const { keep, send } = await wallet.send(4, proofs, { includeDleq: false }); + send.forEach(p => { + expect(p.dleq).toBeUndefined(); + }); + keep.forEach(p => { + expect(p.dleq).toBeDefined(); + expect(p.dleq?.r).toBeDefined(); + }); + }); }); From 582172f0ee87fbe81b56a0475e0248ff31921ee0 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 7 Nov 2024 08:55:43 +0100 Subject: [PATCH 17/27] test not enough funds when dleq missing and includeDleq true --- test/integration.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/integration.test.ts b/test/integration.test.ts index 6353ef54..28b13a9a 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -321,4 +321,23 @@ describe('dleq', () => { expect(p.dleq?.r).toBeDefined(); }); }); + test('send not enough proofs when dleq is required', async () => { + const mint = new CashuMint(mintUrl); + const wallet = new CashuWallet(mint); + const NUT12 = (await mint.getInfo()).nuts['12']; + if (NUT12 == undefined || !NUT12.supported) { + throw new Error("Cannot run this test: mint does not support NUT12"); + } + + const mintRequest = await wallet.createMintQuote(8); + let { proofs } = await wallet.mintProofs(8, mintRequest.quote); + + // strip dleq + proofs = proofs.map(p => { + return { ...p, dleq: undefined }; + }); + + const exc = await wallet.send(4, proofs, { includeDleq: true }).catch(e => e); + expect(exc).toEqual(new Error("Not enough funds available to send")); + }); }); From 2bfb5a01e45768154a8696c45e9eb9403359c73f Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 7 Nov 2024 11:22:20 +0100 Subject: [PATCH 18/27] npm run format --- src/CashuWallet.ts | 8 ++++---- src/utils.ts | 4 ++-- test/integration.test.ts | 32 ++++++++++++++++---------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 6f117495..6ba1621c 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -353,7 +353,7 @@ class CashuWallet { // strip dleq if explicitly told so if (!options?.includeDleq) { send = send.map((p: Proof) => { - return {...p, dleq: undefined }; + return { ...p, dleq: undefined }; }); } @@ -407,7 +407,7 @@ class CashuWallet { const { keep, send } = this.selectProofsToSend( smallerProofs.slice(1), remainder, - includeFees, + includeFees ); selectedProofs.push(...send); returnedProofs.push(...keep); @@ -417,7 +417,7 @@ class CashuWallet { if (sumProofs(selectedProofs) < amountToSend + selectedFeePPK && nextBigger) { selectedProofs = [nextBigger]; } - + return { keep: proofs.filter((p: Proof) => !selectedProofs.includes(p)), send: selectedProofs @@ -1038,7 +1038,7 @@ class CashuWallet { : ({ s: bytesToHex(dleq.s), e: bytesToHex(dleq.e), - r: numberToHexPadded64(dleq.r ?? BigInt(0)), + r: numberToHexPadded64(dleq.r ?? BigInt(0)) } as SerializedDLEQ); return serializedProof; }); diff --git a/src/utils.ts b/src/utils.ts index 471e7d28..0ac43cc3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -153,8 +153,8 @@ export function hexToNumber(hex: string): bigint { } /** - * - * @param number (bigint) to conver to hex + * Converts a number to a hex string of 64 characters. + * @param number (bigint) to conver to hex * @returns hex string start-padded to 64 characters */ export function numberToHexPadded64(number: bigint): string { diff --git a/test/integration.test.ts b/test/integration.test.ts index 28b13a9a..0597dd55 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -259,14 +259,14 @@ describe('dleq', () => { const mint = new CashuMint(mintUrl); const NUT12 = (await mint.getInfo()).nuts['12']; if (NUT12 == undefined || !NUT12.supported) { - throw new Error("Cannot run this test: mint does not support NUT12"); + throw new Error('Cannot run this test: mint does not support NUT12'); } const wallet = new CashuWallet(mint); const mintRequest = await wallet.createMintQuote(3000); const { proofs } = await wallet.mintProofs(3000, mintRequest.quote); - proofs.forEach(p => { + proofs.forEach((p) => { expect(p).toHaveProperty('dleq'); expect(p.dleq).toHaveProperty('s'); expect(p.dleq).toHaveProperty('e'); @@ -279,7 +279,7 @@ describe('dleq', () => { const wallet = new CashuWallet(mint); const NUT12 = (await mint.getInfo()).nuts['12']; if (NUT12 == undefined || !NUT12.supported) { - throw new Error("Cannot run this test: mint does not support NUT12"); + throw new Error('Cannot run this test: mint does not support NUT12'); } const mintRequest = await wallet.createMintQuote(8); @@ -287,36 +287,36 @@ describe('dleq', () => { const { keep, send } = await wallet.send(4, proofs, { includeDleq: true }); - send.forEach(p => { + send.forEach((p) => { expect(p.dleq).toBeDefined(); expect(p.dleq?.r).toBeDefined(); }); - + const token = { mint: mint.mintUrl, proofs: send } as Token; const encodedToken = getEncodedTokenV4(token); - const newProofs = await wallet.receive(encodedToken, { requireDleq: true }) + const newProofs = await wallet.receive(encodedToken, { requireDleq: true }); console.log(getEncodedTokenV4(token)); expect(newProofs).toBeDefined(); - }); - test('send strip dleq', async() => { + }); + test('send strip dleq', async () => { const mint = new CashuMint(mintUrl); const wallet = new CashuWallet(mint); const NUT12 = (await mint.getInfo()).nuts['12']; if (NUT12 == undefined || !NUT12.supported) { - throw new Error("Cannot run this test: mint does not support NUT12"); + throw new Error('Cannot run this test: mint does not support NUT12'); } const mintRequest = await wallet.createMintQuote(8); const { proofs } = await wallet.mintProofs(8, mintRequest.quote); const { keep, send } = await wallet.send(4, proofs, { includeDleq: false }); - send.forEach(p => { + send.forEach((p) => { expect(p.dleq).toBeUndefined(); }); - keep.forEach(p => { + keep.forEach((p) => { expect(p.dleq).toBeDefined(); expect(p.dleq?.r).toBeDefined(); }); @@ -326,18 +326,18 @@ describe('dleq', () => { const wallet = new CashuWallet(mint); const NUT12 = (await mint.getInfo()).nuts['12']; if (NUT12 == undefined || !NUT12.supported) { - throw new Error("Cannot run this test: mint does not support NUT12"); + throw new Error('Cannot run this test: mint does not support NUT12'); } const mintRequest = await wallet.createMintQuote(8); let { proofs } = await wallet.mintProofs(8, mintRequest.quote); - // strip dleq - proofs = proofs.map(p => { + // strip dleq + proofs = proofs.map((p) => { return { ...p, dleq: undefined }; }); - const exc = await wallet.send(4, proofs, { includeDleq: true }).catch(e => e); - expect(exc).toEqual(new Error("Not enough funds available to send")); + const exc = await wallet.send(4, proofs, { includeDleq: true }).catch((e) => e); + expect(exc).toEqual(new Error('Not enough funds available to send')); }); }); From 8e7a58d1d57435074f2348d02db0833bc92cd2ba Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 7 Nov 2024 12:24:26 +0100 Subject: [PATCH 19/27] test receive with invalid dleq --- test/integration.test.ts | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index 0597dd55..04f24661 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -2,7 +2,14 @@ import { CashuMint } from '../src/CashuMint.js'; import { CashuWallet } from '../src/CashuWallet.js'; import dns from 'node:dns'; -import { deriveKeysetId, getEncodedToken, getEncodedTokenV4, sumProofs } from '../src/utils.js'; +import { + deriveKeysetId, + getEncodedToken, + getEncodedTokenV4, + hexToNumber, + numberToHexPadded64, + sumProofs +} from '../src/utils.js'; import { secp256k1 } from '@noble/curves/secp256k1'; import { bytesToHex } from '@noble/curves/abstract/utils'; import { CheckStateEnum, MeltQuoteState, Token } from '../src/model/types/index.js'; @@ -340,4 +347,33 @@ describe('dleq', () => { const exc = await wallet.send(4, proofs, { includeDleq: true }).catch((e) => e); expect(exc).toEqual(new Error('Not enough funds available to send')); }); + test('receive with invalid dleq', async () => { + const mint = new CashuMint(mintUrl); + const keys = await mint.getKeys(); + const wallet = new CashuWallet(mint); + const NUT12 = (await mint.getInfo()).nuts['12']; + if (NUT12 == undefined || !NUT12.supported) { + throw new Error('Cannot run this test: mint does not support NUT12'); + } + + const mintRequest = await wallet.createMintQuote(8); + let { proofs } = await wallet.mintProofs(8, mintRequest.quote); + + // alter dleq signature + proofs.forEach((p) => { + if (p.dleq != undefined) { + const s = hexToNumber(p.dleq.s) + BigInt(1); + p.dleq.s = numberToHexPadded64(s); + } + }); + + const token = { + mint: mint.mintUrl, + proofs: proofs + } as Token; + + const key = keys.keysets.filter((k) => k.id === proofs[0].id)[0].keys[proofs[0].amount]; + const exc = await wallet.receive(token, { requireDleq: true }).catch((e) => e); + expect(exc).toEqual(new Error(`0-th DLEQ proof is invalid for key ${key}`)); + }); }); From c70b2c6ddc9306a40941d6625a4215c78c096408 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 8 Nov 2024 16:58:16 +0100 Subject: [PATCH 20/27] requested changes --- src/CashuWallet.ts | 69 ++++++++++------------------------- src/model/BlindedSignature.ts | 2 +- src/utils.ts | 68 ++++++++++++++++++++++++++-------- test/integration.test.ts | 2 +- 4 files changed, 74 insertions(+), 67 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 6ba1621c..58550197 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -28,8 +28,8 @@ import { splitAmount, sumProofs, getKeepAmounts, - hexToNumber, - numberToHexPadded64 + numberToHexPadded64, + hasValidDleq } from './utils.js'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; import { @@ -265,7 +265,9 @@ class CashuWallet { } const keys = await this.getKeys(options?.keysetId); if (options?.requireDleq) { - this.requireDLEQ(token, keys); + if (token.proofs.some((p: Proof) => !hasValidDleq(p, keys))) { + throw new Error("Token contains proofs with invalid DLEQ") + } } const amount = sumProofs(token.proofs) - this.getFeesForProofs(token.proofs); const { payload, blindingData } = this.createSwapPayload( @@ -317,7 +319,7 @@ class CashuWallet { includeDleq?: boolean; } ): Promise { - if (options?.includeDleq ?? false) { + if (options?.includeDleq) { // only pick the ones with a DLEQ proof proofs = proofs.filter((p: Proof) => p.dleq != undefined); } @@ -351,7 +353,7 @@ class CashuWallet { keep = keepProofsSelect.concat(keep); // strip dleq if explicitly told so - if (!options?.includeDleq) { + if (options?.includeDleq === false) { send = send.map((p: Proof) => { return { ...p, dleq: undefined }; }); @@ -1029,53 +1031,22 @@ class CashuWallet { const secret = secrets[i]; const A = pointFromHex(keyset.keys[p.amount]); const proof = constructProofFromPromise(blindSignature, r, secret, A); - const serializedProof = serializeProof(proof) as Proof; - serializedProof.dleqValid = - dleq == undefined ? undefined : verifyDLEQProof_reblind(secret, dleq, proof.C, A); - serializedProof.dleq = - dleq == undefined - ? undefined - : ({ - s: bytesToHex(dleq.s), - e: bytesToHex(dleq.e), - r: numberToHexPadded64(dleq.r ?? BigInt(0)) - } as SerializedDLEQ); + const serializedProof = { + ...serializeProof(proof), + ...(dleq && { + dleqValid: verifyDLEQProof_reblind(secret, dleq, proof.C, A) + }), + ...(dleq && { + dleq: { + s: bytesToHex(dleq.s), + e: bytesToHex(dleq.e), + r: numberToHexPadded64(dleq.r ?? BigInt(0)) + } as SerializedDLEQ + }) + } as Proof; return serializedProof; }); } - - /** - * Checks that each proof in `token` has a valid DLEQ proof according to - * keyset `keys` - * @param token The token subject to the verification - * @param keys The Mint's keyset to be used for verification - */ - private requireDLEQ(token: Token, keys: MintKeys) { - token.proofs.forEach((p: Proof, i: number) => { - if (p.dleq == undefined) { - throw new Error(`${i}-th proof is missing DLEQ proof`); - } - const dleq = { - e: hexToBytes(p.dleq.e), - s: hexToBytes(p.dleq.s), - r: hexToNumber(p.dleq.r ?? '00') - } as DLEQ; - const key = keys.keys[p.amount]; - if (key == undefined) { - throw new Error(`undefined key for amount ${p.amount}`); - } - if ( - !verifyDLEQProof_reblind( - new TextEncoder().encode(p.secret), - dleq, - pointFromHex(p.C), - pointFromHex(key) - ) - ) { - throw new Error(`${i}-th DLEQ proof is invalid for key ${key}`); - } - }); - } } export { CashuWallet }; diff --git a/src/model/BlindedSignature.ts b/src/model/BlindedSignature.ts index 85ca7f15..e5011677 100644 --- a/src/model/BlindedSignature.ts +++ b/src/model/BlindedSignature.ts @@ -9,7 +9,7 @@ class BlindedSignature { C_: ProjPointType; dleq?: DLEQ; - constructor(id: string, amount: number, C_: ProjPointType, dleq: DLEQ) { + constructor(id: string, amount: number, C_: ProjPointType, dleq?: DLEQ) { this.id = id; this.amount = amount; this.C_ = C_; diff --git a/src/utils.ts b/src/utils.ts index 0ac43cc3..c388ecf0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,6 +7,7 @@ import { import { DeprecatedToken, Keys, + MintKeys, Proof, SerializedDLEQ, Token, @@ -20,6 +21,8 @@ import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; import { sha256 } from '@noble/hashes/sha256'; import { decodeCBOR, encodeCBOR } from './cbor.js'; import { PaymentRequest } from './model/PaymentRequest.js'; +import { DLEQ, pointFromHex } from '@cashu/crypto/modules/common'; +import { verifyDLEQProof_reblind } from '@cashu/crypto/modules/client/NUT12'; /** * Splits the amount into denominations of the provided @param keyset @@ -246,14 +249,13 @@ export function getEncodedTokenV4(token: Token): string { a: p.amount, s: p.secret, c: hexToBytes(p.C), - d: - p.dleq == undefined - ? undefined - : ({ - e: hexToBytes(p.dleq.e), - s: hexToBytes(p.dleq.s), - r: hexToBytes(p.dleq.r ?? '00') - } as V4DLEQTemplate) + ...(p.dleq && { + d: { + e: hexToBytes(p.dleq.e), + s: hexToBytes(p.dleq.s), + r: hexToBytes(p.dleq.r ?? '00') + } as V4DLEQTemplate + }), }) ) }) @@ -322,14 +324,13 @@ export function handleTokens(token: string): Token { C: bytesToHex(p.c), amount: p.a, id: bytesToHex(t.i), - dleq: - p.d == undefined - ? undefined - : ({ - e: bytesToHex(p.d.e), - s: bytesToHex(p.d.s), - r: bytesToHex(p.d.r) - } as SerializedDLEQ) + ...(p.d && { + dleq: { + r: bytesToHex(p.d.r), + s: bytesToHex(p.d.s), + e: bytesToHex(p.d.e), + } as SerializedDLEQ + }) }); }) ); @@ -397,3 +398,38 @@ export function sumProofs(proofs: Array) { export function decodePaymentRequest(paymentRequest: string) { return PaymentRequest.fromEncodedRequest(paymentRequest); } + +/** + * Checks that the proof has a valid DLEQ proof according to + * keyset `keys` + * @param proof The proof subject to verification + * @param keyset The Mint's keyset to be used for verification + * @returns true if verification succeeded, false otherwise + * @throws Error if @param proof does not match any key in @param keyset + */ +export function hasValidDleq(proof: Proof, keyset: MintKeys): boolean { + if (proof.dleq == undefined) { + return false; + } + const dleq = { + e: hexToBytes(proof.dleq.e), + s: hexToBytes(proof.dleq.s), + r: hexToNumber(proof.dleq.r ?? '00') + } as DLEQ; + const key = keyset.keys[proof.amount]; + if (key == undefined) { + throw new Error(`undefined key for amount ${proof.amount}`); + } + if ( + !verifyDLEQProof_reblind( + new TextEncoder().encode(proof.secret), + dleq, + pointFromHex(proof.C), + pointFromHex(key) + ) + ) { + return false; + } + + return true; +} \ No newline at end of file diff --git a/test/integration.test.ts b/test/integration.test.ts index 04f24661..2b87932a 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -374,6 +374,6 @@ describe('dleq', () => { const key = keys.keysets.filter((k) => k.id === proofs[0].id)[0].keys[proofs[0].amount]; const exc = await wallet.receive(token, { requireDleq: true }).catch((e) => e); - expect(exc).toEqual(new Error(`0-th DLEQ proof is invalid for key ${key}`)); + expect(exc).toEqual(new Error("Token contains proofs with invalid DLEQ")); }); }); From d2da84204b546ff0ee1243f09fe0aaaaf194b9a0 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 8 Nov 2024 17:03:07 +0100 Subject: [PATCH 21/27] clean tests --- test/integration.test.ts | 1 - test/utils.test.ts | 15 +++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index 2b87932a..37d2fc4d 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -372,7 +372,6 @@ describe('dleq', () => { proofs: proofs } as Token; - const key = keys.keysets.filter((k) => k.id === proofs[0].id)[0].keys[proofs[0].amount]; const exc = await wallet.receive(token, { requireDleq: true }).catch((e) => e); expect(exc).toEqual(new Error("Token contains proofs with invalid DLEQ")); }); diff --git a/test/utils.test.ts b/test/utils.test.ts index 64017954..77838393 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -212,8 +212,7 @@ describe('test decode token', () => { secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', id: '00ad268c4d1f5826', - amount: 1, - dleq: undefined + amount: 1 } ] }; @@ -233,22 +232,19 @@ describe('test decode token', () => { secret: 'acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388', C: '0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf', id: '00ffd48b8f5ecf80', - amount: 1, - dleq: undefined + amount: 1 }, { secret: '1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee', C: '023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d', id: '00ad268c4d1f5826', - amount: 2, - dleq: undefined + amount: 2 }, { secret: '56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57', C: '0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63', id: '00ad268c4d1f5826', - amount: 1, - dleq: undefined + amount: 1 } ] }; @@ -281,8 +277,7 @@ describe('test v4 encoding', () => { secret: '9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e', C: '038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792', id: '00ad268c4d1f5826', - amount: 1, - dleq: undefined + amount: 1 } ], unit: 'sat' From 266c8aa789df2b429e21520fd3caf2f14d0dcee8 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 8 Nov 2024 18:17:48 +0100 Subject: [PATCH 22/27] test for `hasValidDleq` in utils tests. --- src/utils.ts | 4 ++-- test/utils.test.ts | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index c388ecf0..ccd0b62d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -416,10 +416,10 @@ export function hasValidDleq(proof: Proof, keyset: MintKeys): boolean { s: hexToBytes(proof.dleq.s), r: hexToNumber(proof.dleq.r ?? '00') } as DLEQ; - const key = keyset.keys[proof.amount]; - if (key == undefined) { + if (!hasCorrespondingKey(proof.amount, keyset.keys)) { throw new Error(`undefined key for amount ${proof.amount}`); } + const key = keyset.keys[proof.amount]; if ( !verifyDLEQProof_reblind( new TextEncoder().encode(proof.secret), diff --git a/test/utils.test.ts b/test/utils.test.ts index 77838393..7596d1ad 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,6 +1,12 @@ -import { Token, Keys, Proof } from '../src/model/types/index.js'; +import { blindMessage, constructProofFromPromise, serializeProof } from '@cashu/crypto/modules/client'; +import { Keys, Proof } from '../src/model/types/index.js'; import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; +import { createDLEQProof } from '@cashu/crypto/modules/mint/NUT12'; +import { hasValidDleq, hexToNumber, numberToHexPadded64 } from '../src/utils.js'; +import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; +import { createBlindSignature, getPubKeyFromPrivKey } from '@cashu/crypto/modules/mint'; +import { pointFromBytes } from '@cashu/crypto/modules/common'; const keys: Keys = {}; for (let i = 1; i <= 2048; i *= 2) { @@ -353,3 +359,40 @@ describe('test output selection', () => { expect(amountsToKeep).toEqual([1, 1, 2, 2, 8, 8]); }); }); +describe('test zero-knowledge utilities', () => { + test('has valid dleq', () => { + // create private public key pair + const privkey = hexToBytes('1'.padStart(64, '0')); + const pubkey = pointFromBytes(getPubKeyFromPrivKey(privkey)); + + // make up a secret + const fakeSecret = new TextEncoder().encode('fakeSecret'); + // make up blinding factor + const r = hexToNumber('123456'.padStart(64, '0')); + // blind secret + const fakeBlindedMessage = blindMessage(fakeSecret, r) + // construct DLEQ + const fakeDleq = createDLEQProof(fakeBlindedMessage.B_, privkey); + // blind signature + const fakeBlindSignature = createBlindSignature(fakeBlindedMessage.B_, privkey, 1, '00') + // unblind + const proof = constructProofFromPromise(fakeBlindSignature, r, fakeSecret, pubkey); + // serialize + const serializedProof = { + ...serializeProof(proof), + dleq: { + r: numberToHexPadded64(r), + e: bytesToHex(fakeDleq.e), + s: bytesToHex(fakeDleq.s), + } + } as Proof; + // use hasValidDleq to verify DLEQ + const keyset = { + id: '00', + unit: 'sat', + keys: {[1]: pubkey.toHex(true)}, + }; + const validDleq = hasValidDleq(serializedProof, keyset); + expect(validDleq).toBe(true); + }) +}); From efc73ec9e5566b0b6a0509cbe819c0be71ba28af Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 8 Nov 2024 18:18:13 +0100 Subject: [PATCH 23/27] npm run format --- src/CashuWallet.ts | 2 +- src/utils.ts | 8 ++++---- test/integration.test.ts | 2 +- test/utils.test.ts | 16 ++++++++++------ 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 58550197..1e3cfb2b 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -266,7 +266,7 @@ class CashuWallet { const keys = await this.getKeys(options?.keysetId); if (options?.requireDleq) { if (token.proofs.some((p: Proof) => !hasValidDleq(p, keys))) { - throw new Error("Token contains proofs with invalid DLEQ") + throw new Error('Token contains proofs with invalid DLEQ'); } } const amount = sumProofs(token.proofs) - this.getFeesForProofs(token.proofs); diff --git a/src/utils.ts b/src/utils.ts index ccd0b62d..7c2f3137 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -255,7 +255,7 @@ export function getEncodedTokenV4(token: Token): string { s: hexToBytes(p.dleq.s), r: hexToBytes(p.dleq.r ?? '00') } as V4DLEQTemplate - }), + }) }) ) }) @@ -328,7 +328,7 @@ export function handleTokens(token: string): Token { dleq: { r: bytesToHex(p.d.r), s: bytesToHex(p.d.s), - e: bytesToHex(p.d.e), + e: bytesToHex(p.d.e) } as SerializedDLEQ }) }); @@ -405,7 +405,7 @@ export function decodePaymentRequest(paymentRequest: string) { * @param proof The proof subject to verification * @param keyset The Mint's keyset to be used for verification * @returns true if verification succeeded, false otherwise - * @throws Error if @param proof does not match any key in @param keyset + * @throws Error if @param proof does not match any key in @param keyset */ export function hasValidDleq(proof: Proof, keyset: MintKeys): boolean { if (proof.dleq == undefined) { @@ -432,4 +432,4 @@ export function hasValidDleq(proof: Proof, keyset: MintKeys): boolean { } return true; -} \ No newline at end of file +} diff --git a/test/integration.test.ts b/test/integration.test.ts index 37d2fc4d..e0b0d9a5 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -373,6 +373,6 @@ describe('dleq', () => { } as Token; const exc = await wallet.receive(token, { requireDleq: true }).catch((e) => e); - expect(exc).toEqual(new Error("Token contains proofs with invalid DLEQ")); + expect(exc).toEqual(new Error('Token contains proofs with invalid DLEQ')); }); }); diff --git a/test/utils.test.ts b/test/utils.test.ts index 7596d1ad..cbdfed48 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,4 +1,8 @@ -import { blindMessage, constructProofFromPromise, serializeProof } from '@cashu/crypto/modules/client'; +import { + blindMessage, + constructProofFromPromise, + serializeProof +} from '@cashu/crypto/modules/client'; import { Keys, Proof } from '../src/model/types/index.js'; import * as utils from '../src/utils.js'; import { PUBKEYS } from './consts.js'; @@ -370,11 +374,11 @@ describe('test zero-knowledge utilities', () => { // make up blinding factor const r = hexToNumber('123456'.padStart(64, '0')); // blind secret - const fakeBlindedMessage = blindMessage(fakeSecret, r) + const fakeBlindedMessage = blindMessage(fakeSecret, r); // construct DLEQ const fakeDleq = createDLEQProof(fakeBlindedMessage.B_, privkey); // blind signature - const fakeBlindSignature = createBlindSignature(fakeBlindedMessage.B_, privkey, 1, '00') + const fakeBlindSignature = createBlindSignature(fakeBlindedMessage.B_, privkey, 1, '00'); // unblind const proof = constructProofFromPromise(fakeBlindSignature, r, fakeSecret, pubkey); // serialize @@ -383,16 +387,16 @@ describe('test zero-knowledge utilities', () => { dleq: { r: numberToHexPadded64(r), e: bytesToHex(fakeDleq.e), - s: bytesToHex(fakeDleq.s), + s: bytesToHex(fakeDleq.s) } } as Proof; // use hasValidDleq to verify DLEQ const keyset = { id: '00', unit: 'sat', - keys: {[1]: pubkey.toHex(true)}, + keys: { [1]: pubkey.toHex(true) } }; const validDleq = hasValidDleq(serializedProof, keyset); expect(validDleq).toBe(true); - }) + }); }); From f26bd150ddd8310cfccbb3bfd8a4ccd9b511c25c Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 8 Nov 2024 18:29:40 +0100 Subject: [PATCH 24/27] test no matching key --- test/utils.test.ts | 66 ++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/test/utils.test.ts b/test/utils.test.ts index cbdfed48..cafa3de0 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -364,33 +364,33 @@ describe('test output selection', () => { }); }); describe('test zero-knowledge utilities', () => { - test('has valid dleq', () => { - // create private public key pair - const privkey = hexToBytes('1'.padStart(64, '0')); - const pubkey = pointFromBytes(getPubKeyFromPrivKey(privkey)); + // create private public key pair + const privkey = hexToBytes('1'.padStart(64, '0')); + const pubkey = pointFromBytes(getPubKeyFromPrivKey(privkey)); + + // make up a secret + const fakeSecret = new TextEncoder().encode('fakeSecret'); + // make up blinding factor + const r = hexToNumber('123456'.padStart(64, '0')); + // blind secret + const fakeBlindedMessage = blindMessage(fakeSecret, r); + // construct DLEQ + const fakeDleq = createDLEQProof(fakeBlindedMessage.B_, privkey); + // blind signature + const fakeBlindSignature = createBlindSignature(fakeBlindedMessage.B_, privkey, 1, '00'); + // unblind + const proof = constructProofFromPromise(fakeBlindSignature, r, fakeSecret, pubkey); + // serialize + const serializedProof = { + ...serializeProof(proof), + dleq: { + r: numberToHexPadded64(r), + e: bytesToHex(fakeDleq.e), + s: bytesToHex(fakeDleq.s) + } + } as Proof; - // make up a secret - const fakeSecret = new TextEncoder().encode('fakeSecret'); - // make up blinding factor - const r = hexToNumber('123456'.padStart(64, '0')); - // blind secret - const fakeBlindedMessage = blindMessage(fakeSecret, r); - // construct DLEQ - const fakeDleq = createDLEQProof(fakeBlindedMessage.B_, privkey); - // blind signature - const fakeBlindSignature = createBlindSignature(fakeBlindedMessage.B_, privkey, 1, '00'); - // unblind - const proof = constructProofFromPromise(fakeBlindSignature, r, fakeSecret, pubkey); - // serialize - const serializedProof = { - ...serializeProof(proof), - dleq: { - r: numberToHexPadded64(r), - e: bytesToHex(fakeDleq.e), - s: bytesToHex(fakeDleq.s) - } - } as Proof; - // use hasValidDleq to verify DLEQ + test('has valid dleq', () => { const keyset = { id: '00', unit: 'sat', @@ -399,4 +399,18 @@ describe('test zero-knowledge utilities', () => { const validDleq = hasValidDleq(serializedProof, keyset); expect(validDleq).toBe(true); }); + test('has valid dleq with no matching key', () => { + const keyset = { + id: '00', + unit: 'sat', + keys: { [2]: pubkey.toHex(true) } + }; + let exc; + try { + hasValidDleq(serializedProof, keyset); + } catch (e) { + exc = e; + } + expect(exc).toEqual(new Error('undefined key for amount 1')); + }); }); From 06fdaab06c8600128060a064f625ce8543f5fee5 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 13 Nov 2024 10:37:12 +0100 Subject: [PATCH 25/27] use new syntax for serializedDLEQ --- src/model/BlindedSignature.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/model/BlindedSignature.ts b/src/model/BlindedSignature.ts index e5011677..684fa50f 100644 --- a/src/model/BlindedSignature.ts +++ b/src/model/BlindedSignature.ts @@ -2,6 +2,7 @@ import { ProjPointType } from '@noble/curves/abstract/weierstrass'; import { SerializedBlindedSignature } from './types/index.js'; import { DLEQ } from '@cashu/crypto/modules/common'; import { bytesToHex } from '@noble/hashes/utils.js'; +import { numberToHexPadded64 } from '../utils.js'; class BlindedSignature { id: string; @@ -21,14 +22,13 @@ class BlindedSignature { id: this.id, amount: this.amount, C_: this.C_.toHex(true), - dleq: - this.dleq == undefined - ? undefined - : { - s: bytesToHex(this.dleq.s), - e: bytesToHex(this.dleq.e), - r: this.dleq.r?.toString(16) - } + ...(this.dleq && { + dleq: { + s: bytesToHex(this.dleq.s), + e: bytesToHex(this.dleq.e), + r: numberToHexPadded64(this.dleq.r ?? BigInt(0)) + } + }) }; } } From af68d5766a0ed8e8fe561918ce5e62c1c3b46e0c Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 13 Nov 2024 11:55:37 +0000 Subject: [PATCH 26/27] abstracted strip util + changed conditions --- src/CashuWallet.ts | 16 +++++----------- src/utils.ts | 13 +++++++++++++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index 1e3cfb2b..e436e336 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -29,7 +29,8 @@ import { sumProofs, getKeepAmounts, numberToHexPadded64, - hasValidDleq + hasValidDleq, + stripDleq } from './utils.js'; import { hashToCurve, pointFromHex } from '@cashu/crypto/modules/common'; import { @@ -320,7 +321,6 @@ class CashuWallet { } ): Promise { if (options?.includeDleq) { - // only pick the ones with a DLEQ proof proofs = proofs.filter((p: Proof) => p.dleq != undefined); } if (sumProofs(proofs) < amount) { @@ -352,11 +352,8 @@ class CashuWallet { let { keep, send } = await this.swap(amount, sendProofs, options); keep = keepProofsSelect.concat(keep); - // strip dleq if explicitly told so - if (options?.includeDleq === false) { - send = send.map((p: Proof) => { - return { ...p, dleq: undefined }; - }); + if (!options?.includeDleq) { + send = stripDleq(send); } return { keep, send }; @@ -366,11 +363,8 @@ class CashuWallet { throw new Error('Not enough funds available to send'); } - // strip dleq if explicitly told so if (!options?.includeDleq) { - sendProofOffline.forEach((p: Proof) => { - p.dleq = undefined; - }); + return { keep: keepProofsOffline, send: stripDleq(sendProofOffline) }; } return { keep: keepProofsOffline, send: sendProofOffline }; diff --git a/src/utils.ts b/src/utils.ts index 7c2f3137..32f5a89b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -399,6 +399,19 @@ export function decodePaymentRequest(paymentRequest: string) { return PaymentRequest.fromEncodedRequest(paymentRequest); } +/** + * Removes all traces of DLEQs from a list of proofs + * @param proofs The list of proofs that dleq should be stripped from + */ +export function stripDleq(proofs: Array): Array> { + return proofs.map((p) => { + const newP = { ...p }; + delete newP['dleq']; + delete newP['dleqValid']; + return newP; + }); +} + /** * Checks that the proof has a valid DLEQ proof according to * keyset `keys` From 31d383259dae7139dc511be6d9c674809d1900cb Mon Sep 17 00:00:00 2001 From: Egge Date: Wed, 13 Nov 2024 12:10:20 +0000 Subject: [PATCH 27/27] use stripDleq everywhere --- src/CashuWallet.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/CashuWallet.ts b/src/CashuWallet.ts index e436e336..54bcb0cd 100644 --- a/src/CashuWallet.ts +++ b/src/CashuWallet.ts @@ -767,10 +767,9 @@ class CashuWallet { options.privkey ).map((p: NUT11Proof) => serializeProof(p)); } - // Strip DLEQs if any - proofsToSend = proofsToSend.map((p: Proof) => { - return { ...p, dleq: undefined }; - }); + + proofsToSend = stripDleq(proofsToSend); + const meltPayload: MeltPayload = { quote: meltQuote.quote, inputs: proofsToSend, @@ -846,10 +845,7 @@ class CashuWallet { ).map((p: NUT11Proof) => serializeProof(p)); } - // Strip DLEQs if any - proofsToSend = proofsToSend.map((p: Proof) => { - return { ...p, dleq: undefined }; - }); + proofsToSend = stripDleq(proofsToSend); // join keepBlindedMessages and sendBlindedMessages const blindingData: BlindingData = {