Improved validation of keysets provided by the mints

This commit is contained in:
minibits-cash
2025-07-30 16:33:34 +02:00
parent 324dc07952
commit 378e85e697
6 changed files with 76 additions and 58 deletions
+1 -1
View File
@@ -1 +1 @@
0198388d-d8dc-72f2-b3e1-89f896656f07 0198586c-6212-732d-a2cb-cf9537d9aab3
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "minibits_wallet", "name": "minibits_wallet",
"version": "0.2.2-beta.29", "version": "0.2.2-beta.30",
"private": true, "private": true,
"scripts": { "scripts": {
"android:clean": "cd android && ./gradlew clean", "android:clean": "cd android && ./gradlew clean",
+9 -6
View File
@@ -39,21 +39,24 @@ export const ErrorModal: FC<ErrorModalProps> = function ({ error }) {
} }
} }
const backgroundColor = useThemeColor('error') const bg = useThemeColor('error')
return ( return (
<BottomModal <BottomModal
isVisible={isErrorVisible} isVisible={isErrorVisible}
onBackdropPress={onClose} onBackdropPress={onClose}
onBackButtonPress={onClose} onBackButtonPress={onClose}
style={{ backgroundColor }}
ContentComponent={ ContentComponent={
<> <>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: spacing.small }}> <View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: spacing.small }}>
<Icon icon="faInfoCircle" size={spacing.large} color="white" /> <Icon icon="faTriangleExclamation" size={spacing.large} color={bg} />
<Text style={{ color: 'white', marginLeft: spacing.small }}>{error.name}</Text> <Text style={{ color: 'white', marginLeft: spacing.small }}>{error.name}</Text>
</View> </View>
<ScrollView> <ScrollView
style={{
//height: spacing.screenHeight * 0.1,
maxHeight: spacing.screenHeight * 0.07,
}}>
<Text style={{ color: 'white', marginBottom: spacing.small }}>{error.message}</Text> <Text style={{ color: 'white', marginBottom: spacing.small }}>{error.message}</Text>
</ScrollView> </ScrollView>
{error.params && isObj(error.params) && ( {error.params && isObj(error.params) && (
+41 -37
View File
@@ -222,51 +222,55 @@ export const MintModel = types
})) }))
.actions(self => ({ .actions(self => ({
initKeyset(keyset: CashuMintKeyset, allKeysetIds: string[]) { initKeyset(keyset: CashuMintKeyset, allKeysetIds: string[]) {
// ATTN: const mintsStore = getRootStore(self).mintsStore does not work here (may be because it is being called from within loop)
// Do not add unit the wallet does not have configured // Do not add unit the wallet does not have configured
try {
if(!self.isUnitSupported(keyset.unit as MintUnit)) { if(!self.isUnitSupported(keyset.unit as MintUnit)) {
throw new AppError(Err.VALIDATION_ERROR, `Unsupported unit provided by the mint: ${keyset.unit}`) throw new AppError(
} Err.VALIDATION_ERROR,
`Unsupported unit provided by the mint`,
const existing = self.keysets.find(k => k.id === keyset.id) {caller: 'initKeyset', unit: keyset.unit}
)
}
const existing = self.keysets.find(k => k.id === keyset.id)
if(existing) { if(existing) {
if (existing.unit !== keyset.unit) { if (existing.unit !== keyset.unit) {
throw new AppError(
Err.VALIDATION_ERROR,
`Keyset unit mismatch, got ${keyset.unit}, expected ${existing.unit}`,
{caller: 'initKeyset'}
)
}
if(keyset.input_fee_ppk && existing.input_fee_ppk !== keyset.input_fee_ppk) {
self.setInputFeePpk(existing.id, keyset.input_fee_ppk)
}
return existing
}
// Prevent keysetId collision with other mints
if(CashuUtils.isCollidingKeysetId(keyset.id, allKeysetIds)) {
throw new AppError( throw new AppError(
Err.VALIDATION_ERROR, Err.VALIDATION_ERROR,
`KeysetId validation failed, collision detected for ${keyset.id}`, `Keyset unit mismatch.`,
{caller: 'initKeyset'} {caller: 'initKeyset', existingUnit: existing.unit, keysetUnit: keyset.unit}
) )
} }
if(!keyset.input_fee_ppk) { if(keyset.input_fee_ppk && existing.input_fee_ppk !== keyset.input_fee_ppk) {
keyset.input_fee_ppk = 0 self.setInputFeePpk(existing.id, keyset.input_fee_ppk)
} }
self.addKeyset(keyset) return existing
self.addUnit(keyset.unit as MintUnit) }
self.createProofsCounter(keyset)
log.trace('[initKeyset]', {newKeyset: keyset}) // Prevent keysetId collision with other mints
} catch (e: any) { if(CashuUtils.isCollidingKeysetId(keyset.id, allKeysetIds)) {
throw new AppError(Err.WALLET_ERROR, '[initKeyset] ' + e.message) throw new AppError(
} Err.VALIDATION_ERROR,
`KeysetId validation failed, collision detected.`,
{caller: 'initKeyset', keysetId: keyset.id}
)
}
if(!keyset.input_fee_ppk) {
keyset.input_fee_ppk = 0
}
self.addKeyset(keyset)
self.addUnit(keyset.unit as MintUnit)
self.createProofsCounter(keyset)
log.trace('[initKeyset]', {newKeyset: keyset})
}, },
initKeys(key: CashuMintKeys) { initKeys(key: CashuMintKeys) {
// Do not add unit the wallet does not have configured // Do not add unit the wallet does not have configured
@@ -432,7 +436,7 @@ export const MintModel = types
const counters = self.proofsCounters.filter(c => c.inFlightRequests && c.inFlightRequests.length > 0) const counters = self.proofsCounters.filter(c => c.inFlightRequests && c.inFlightRequests.length > 0)
return counters as MintProofsCounter[] return counters as MintProofsCounter[]
}, },
get allInFLightRequests(): InFlightRequest[] { get allInFlightRequests(): InFlightRequest[] {
const requests = self.proofsCounters const requests = self.proofsCounters
.flatMap((counter) => counter.inFlightRequests) // Combine all `inFlightRequests` arrays .flatMap((counter) => counter.inFlightRequests) // Combine all `inFlightRequests` arrays
+23 -12
View File
@@ -6,6 +6,7 @@ import type {
PaymentRequest as CashuPaymentRequest, PaymentRequest as CashuPaymentRequest,
PaymentRequestPayload, PaymentRequestPayload,
} from '@cashu/cashu-ts' } from '@cashu/cashu-ts'
import { bytesToHex } from '@noble/hashes/utils'
import AppError, {Err} from '../../utils/AppError' import AppError, {Err} from '../../utils/AppError'
import { getDecodedToken } from '@cashu/cashu-ts' import { getDecodedToken } from '@cashu/cashu-ts'
import {Proof} from '../../models/Proof' import {Proof} from '../../models/Proof'
@@ -13,6 +14,7 @@ import { log } from '../logService'
import { decodePaymentRequest, sumProofs } from '@cashu/cashu-ts/src/utils' import { decodePaymentRequest, sumProofs } from '@cashu/cashu-ts/src/utils'
import { NostrClient } from '../nostrService' import { NostrClient } from '../nostrService'
import { getUnixTime } from 'date-fns/getUnixTime' import { getUnixTime } from 'date-fns/getUnixTime'
import { Text } from '../../components'
export {CashuProof} export {CashuProof}
@@ -276,35 +278,42 @@ const validateMintKeys = function (keys: object): boolean {
} }
} }
function getKeysetIdInt(keysetIdHex: string) { function getKeysetIdInt(keysetId: string): bigint {
return parseInt(`0x${keysetIdHex}`, 16) % (2 ** 31 - 1) if (/^[0-9a-fA-F]+$/.test(keysetId)) {
return BigInt(`0x${keysetId}`) % BigInt(2 ** 31 - 1)
} else {
const bin = atob(keysetId)
const hex = bytesToHex(new TextEncoder().encode(bin))
return BigInt(`0x${hex}`) % BigInt(2 ** 31 - 1)
}
} }
function isCollidingKeysetId( function isCollidingKeysetId(
newKeysetIdHex: string, newKeysetId: string,
storedKeysetIds: string[], storedKeysetIds: string[],
) { ) {
const newKeysetIdInt = getKeysetIdInt(newKeysetIdHex) const newKeysetIdInt = getKeysetIdInt(newKeysetId)
return storedKeysetIds.some((storedId) => { return storedKeysetIds.some((storedId) => {
const storedKeysetIdInt = getKeysetIdInt(storedId)
if (storedId === newKeysetIdHex) { if (storedId === newKeysetId) {
// Colliding keyset ID! // Colliding keyset ID!
log.error('[isCollidingKeysetId] Colliding keyset ID', { log.error('[isCollidingKeysetId] Colliding keyset ID', {
newKeysetIdHex, newKeysetId,
storedId, storedId,
newKeysetIdInt,
storedKeysetIdInt,
}) })
return true return true
} }
const storedKeysetIdInt = getKeysetIdInt(storedId)
if (storedKeysetIdInt === newKeysetIdInt) { if (storedKeysetIdInt === newKeysetIdInt) {
// Colliding keyset ID integer! // Colliding keyset ID integer!
log.error('[isCollidingKeysetId] Colliding keyset ID integer', { log.error('[isCollidingKeysetId] Colliding keyset ID integer', {
newKeysetIdHex, newKeysetId,
storedId, storedId,
newKeysetIdInt, newKeysetIdInt: newKeysetIdInt.toString(),
storedKeysetIdInt, storedKeysetIdInt: storedKeysetIdInt.toString(),
}) })
return true return true
@@ -397,3 +406,5 @@ export const CashuUtils = {
isTokenP2PKLocked, isTokenP2PKLocked,
isCollidingKeysetId isCollidingKeysetId
} }
+1 -1
View File
@@ -1164,7 +1164,7 @@ const handleInFlightByMintTask = async function (mint: Mint): Promise<WalletTask
log.trace('[handleInFlightByMintTask]', {mintUrl: mint.mintUrl, inFlightCounters}) log.trace('[handleInFlightByMintTask]', {mintUrl: mint.mintUrl, inFlightCounters})
const allInFlightRequestLength = mint.allInFLightRequests?.length const allInFlightRequestLength = mint.allInFlightRequests?.length
const errors: string[] = [] const errors: string[] = []
if(inFlightCounters && inFlightCounters.length > 0) { if(inFlightCounters && inFlightCounters.length > 0) {