dimesum

Startsida / Blogg / Pengar

Pengar

Sex pengabuggar i utgiftsdelning med flera valutor

· 8 min läsning ·

En granskning den 2026-08-21 fann sex valutadefekter som testerna varit gröna genom, och orsaken var en mening i ett dokument som tyst slutat vara sann.

En inaktuell mening i ett dokument kostade sex pengabuggar. En granskning av flera valutor i Dimesum den 2026-08-21 hittade dem i kod som våra testsviter hade varit gröna genom, inklusive en kravsida som återgav en skuld på ¥6,000 som -₹60.00. Meningen löd "grupper är INR-låsta". Sann fram till W7, falsk i samma stund den levererades, och fortfarande kvar i FOLLOWUPS.md en vecka senare.

En inaktuell mening är farligare än ingen dokumentation alls. En senare granskning tror på den och hoppar över vägarna den täcker, så en felaktig rad gör skada som tystnad aldrig kunde. Ett tomt dokument skickar dig till källan. Ett felaktigt skickar dig någon helt annanstans.

En uppföljning som besannats är värre än en öppen

Dimesum parkerar uppskjutet arbete i FOLLOWUPS.md, en rad per beslut med skälet till att det parkerades. Post A17 sade att grupper var INR-låsta, att en utlandsresa inte kunde registreras i valutan den skedde i, och att korrigeringen väntade på ett grundarbeslut. W7 levererade flera valutor ändå: en utgift kan vara i vilken valuta som helst, och saldon nycklas på (group_id, member_id, currency) av migreringen ledger/00005. Ingen strök raden.

Nästa granskning läste A17, drog slutsatsen att området inte var byggt, och öppnade aldrig koden för avräkning, krav eller landningssida. Tre valutablinda pengavägar fortsatte levereras på styrkan av en enda mening. Inget svårt problem stod i vägen. Docstringen i app/amount.py bar samma påstående, så en läsare fick höra det två gånger.

Regeln vi införde

Stryk en uppföljning i samma commit som avslutar den. En rad som beskriver kod som slutade fungera på det sättet är en omdirigering bort från filen du behövde läsa, vilket kostar mer än att inte säga något alls.

Samma hårdkodade 100 dök upp i tre olika filer

Pengar här är int64 i mindre enheter plus en ISO 4217-kod, och flyttal rör dem aldrig. Heltal är exakta av konstruktion, så den enda riskabla aritmetiken som återstår är omvandlingen mellan större och mindre enheter. Den mindre enheten är inte alltid en hundradel. JPY har ingen alls, KWD har tre decimaler, och varje hårdkodad 100 är ett vad om att användaren stannade hemma.

Python-sidovagnens app/amount.py stod för den första observationen, borttagen före granskningen som en landmina snarare än en aktiv defekt. Den multiplicerade vilken siffra den än läste ur en mening med 100, så ett kvitto på ¥1,200 blev 120,000 mindre enheter. Det avläses som ¥120,000. På en valuta utan mindre enhet blåser konstanten upp någons räkning hundrafalt, och funktionen slår nu istället upp exponenten för begärd valuta.

Ett kvitto på 1,200 yen omvandlat med en hårdkodad multiplikator på 100 och med ISO 4217-exponenttabellen ¥1,200 valuta: JPY FÖRE mindre = större x 100 konstant i modulen 120000 mindre enheter avläses som ¥120,000 EFTER mindre = större x 10^exp JPY exponent = 0 1200 mindre enheter avläses som ¥1,200
Exponentfällan i en bild. En konstant multiplikator på 100 är korrekt för INR och fel med en faktor 100 för JPY, som inte har någon mindre enhet; samma konstant är fel med tio för KWD, som har tre decimaler.

Granskningen fann samma konstant på sidan där en person avgör om ett saldo ska godtas. formatMinor, bakom kravsidan på /m/{token}, dividerade med 100 och klistrade ett rupietecken längst fram. En skuld på ¥6,000 skrevs ut som -₹60.00: fel symbol, en hundradel av beloppet. Dess JSON-syskon /v1/claims fallerade jämsides med den, plattade till en medlems rader per valuta till en enda och behöll vilken valuta kartan än skrev sist.

Den tredje filen är internal/ingestion/store.go, och W8 hittade den när dubblettdetektering utökades, inte granskningen. Dess tolerans på "plus eller minus en rupie" var konstanten 100, en rupie räknad i paise, ett band på plus eller minus ¥100 där JPY inte har någon mindre enhet alls. Alla tre läser exponenten nu.

Att jämföra nakna heltal fick en yen att se ut som en rupie

Dimesums dedupe-lager kontrollerar en ny registrering mot senaste utgifter så att ett importerat kvitto inte kan dubbeldebitera en post som någon skrev in för hand. Frågan bredvid det toleransbandet hade ingen valutafilter alls. Så ¥1,000 matchade ₹1,000, och en import erbjöd sig att ersätta en utgift den inte hade något med att göra. Grupper hade haft flera valutor sedan ledger/00005, så fallet var nåbart snarare än teoretiskt.

Nakna mindre enheter är inte jämförbara mellan valutor, och det är inte toleranser heller. Projektionen filtrerar på valuta nu, och härleder sitt band från en större enhet av den jämförda valutan.

Två motorer svarade på "vem betalar vem", och den andra var valutablind

Den dyra defekten är strukturell, inte aritmetisk. Avräkningens skrivväg planerade över internal/platform/simplify, en andra min-cash-flow-motor vars Balance-struct inte hade något valutafält. Nollsumma per valuta gör även den platta summan noll, så en skuld på ¥300,000 och en skuld på ₹500 tog ut varandra till ingenting och inget skydd avvisade planen. Planen parade sedan ihop en yen-borgenär med en rupie-gäldenär.

Skrivningen gjorde det värre. Tjänsten stämplade Currency: g.DefaultCurrency på varje avräkning, så en skuld på ¥300,000 auktoriserade en INR-journal, och yen-skulden kunde inte regleras alls.

Samma fyra saldon dirigerade av den valutablinda simplify-motorn och av den valutamedvetna settle-motorn SALDON Asha +¥300,000 Bhavna -¥300,000 Chetan +₹500 Dev -₹500 simplify: Balance{MemberID, Minor} ingen valuta på structen, så alla fyra nettar platt 300000 + (-300000) + 500 + (-500) = 0 plan: Bhavna betalar Chetan en yen-gäldenär skickad till en rupie-borgenär settle: Balance{MemberID, money.Amount} partitionera per valuta, dirigera sedan inom varje JPY: Bhavna betalar Asha ¥300,000 INR: Dev betalar Chetan ₹500 avräkningen anger valutan den reglerar simplify är borttagen, inte lagad
Fyra saldon, två motorer. Nollsumma per valuta innebär en platt summa på noll, så en valutablind motor ser en balanserad grupp och dirigerar tryggt en betalning mellan två personer som inte är skyldiga varandra något.

simplify är borttagen och /settle-plan är avvecklad. internal/platform/settle är den enda motorn nu: den partitionerar valutor i konverterbara och icke-konverterbara, konverterar nettosaldon en gång, och dirigerar varje icke-konverterbar valuta i sin egen valör. En avräkning anger valutan den reglerar, och det fältet krävs så snart en grupp innehåller mer än en.

Ett tak på noll lästes som inget tak

Överbetalningsskyddet avvisar en betalning som är större än skulden den reglerar. Skyddet läste outstanding > 0 && amount > outstanding, så ett tak på noll hoppade över jämförelsen helt. Någon som registrerar en betalning mot en skuld som inte finns är just det fall regeln finns för, och det var fallet som slank igenom.

Korrigeringen är en typ, inte ett villkor. OutstandingMinor är nu en *int64, så "ingen beräknade detta" kan inte skrivas på samma sätt som "svaret är noll". Nil hoppar över kontrollen och betyder genuint okänt; varje anropare med liggaråtkomst skickar ett riktigt tal.

Varför en grön svit inte bevisade något

Varje fixtur i dessa vägar använde INR. En valutablind jämförelse är osynlig under ett test med en enda valuta, för med en valuta finns det inget att förväxla. Sviterna var inte svaga, de var smala, och granskningen som hade breddat dem hade skickats bort av A17.

Ände-till-ände-liggarverifieraren delade blindheten, vilket är den del som är värd att behålla. Verifieraren summerade posteringar per medlem utan att gruppera per valuta, så den ropade varg på en frisk grupp med två valutor och summerade till noll över en grupp som var trasig två gånger. Ett falskt negativt är den farliga riktningen. En kontroll byggd på samma antagande som koden kommer alltid att hålla med koden.

Varje defekt som granskningen av flera valutor den 2026-08-21 fann, vad en yen-grupp fick, och vad som levererades.
DefektVarVad en JPY-grupp fickKorrigering
En Balance-struct utan valuta på sigplatform/simplifyen yen-borgenär parad med en rupie-gäldenärsimplify borttagen; settle partitionerar per valuta
Gruppens standardvärde stämplat på varje avräkningavräkningstjänsten skuld på ¥300,000 auktoriserade en INR-journalen avräkning anger valutan den reglerar
outstanding > 0 i överbetalningsskyddetavräkningstjänstett tak på noll blev inget tak alls*int64: nil betyder okänt, noll betyder noll
Dividera med 100 med ett rupieteckengateway formatMinoren skuld på ¥6,000 skrevs ut som -₹60.00symbol och decimaler från valutan
Saldon per valuta tillplattade till en rad/v1/claimsvilken valuta kartan än skrev sisten post per valuta, matchande GET /balances
Posteringar summerade per medlem, valuta bortfallenände-till-ände-liggarverifierareen dubbelt trasig grupp rapporterad som balanseradgruppera per medlem och valuta

Greppa din pengakod efter den bokstavliga 100

Sök i den efter den konstanten, och efter varje jämförelse som ställer två belopp sida vid sida utan en valuta bredvid dem. Åtgärda sedan det billigare, det som förhindrar de nästa sex: stryk en uppföljning i samma commit som avslutar den. Och ta bort en dubblettmotor istället för att laga den. Två svar på "vem betalar vem" är hur ett av dem förblir fel.

Vanliga frågor

Vad är utgiftsdelning med flera valutor?

Utgiftsdelning med flera valutor registrerar varje utgift i valutan den skedde i och håller ett separat saldo per valuta istället för att omvandla allt till en. Dimesum nycklar saldon på grupp, medlem och valuta, så en yen-skuld och en rupie-skuld slås aldrig ihop till ett enda tal. Omvandling är en vy som används för en avräkningsplan, aldrig ett lagrat belopp.

Varför är en hårdkodad 100 farlig i pengakod?

En hårdkodad 100 antar att varje valuta har två decimaler, och flera har det inte. JPY har ingen mindre enhet, så att multiplicera med 100 gör ett kvitto på ¥1,200 till 120,000 mindre enheter, en hundrafaldig uppblåsning snarare än ett avrundningsfel. KWD har tre decimaler, så samma konstant är fel med tio. Läs ISO 4217-exponenten istället.

Hur hindrar man två avräkningsmotorer från att säga emot varandra?

Ta bort en av de två motorerna istället för att stämma av dem, för ett andra svar på vem som betalar vem är hur det första förblir fel. Dimesum körde simplify och settle sida vid sida tills en granskning fann att den första inte hade någon valuta på sin saldotyp, vilket lät en plan para ihop en yen-borgenär med en rupie-gäldenär. simplify togs bort och /settle-plan avvecklades istället för att lagas.

Varför förblev testsviten grön genom sex pengabuggar?

Testsviten förblev grön eftersom varje fixtur i de berörda vägarna använde en enda valuta, och en valutablind jämförelse kan inte fallera när det bara finns en valuta. Ände-till-ände-liggarverifieraren höll samma antagande: den summerade posteringar per medlem utan att gruppera per valuta, så den rapporterade en dubbelt trasig grupp som balanserad. En kontroll byggd på kodens eget antagande håller med koden.

Vad ska man göra med en uppföljningspost när funktionen väl levererats?

Stryk en uppföljningspost i samma commit som avslutar arbetet den beskriver. En uppföljning som tyst har besannats är värre än en öppen, för den pekar en senare läsare bort från koden den brukade beskriva. Dimesum lämnade en post som sade att grupper var INR-låsta i en vecka efter att flera valutor levererats, och nästa granskning hoppade över tre pengavägar på dess ord.