Sex pengabuggar i utgiftsdelning med flera valutor
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.
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.
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.
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.
| Defekt | Var | Vad en JPY-grupp fick | Korrigering |
|---|---|---|---|
En Balance-struct utan valuta på sig | platform/simplify | en yen-borgenär parad med en rupie-gäldenär | simplify borttagen; settle partitionerar per valuta |
| Gruppens standardvärde stämplat på varje avräkning | avräkningstjänst | en skuld på ¥300,000 auktoriserade en INR-journal | en avräkning anger valutan den reglerar |
outstanding > 0 i överbetalningsskyddet | avräkningstjänst | ett tak på noll blev inget tak alls | *int64: nil betyder okänt, noll betyder noll |
| Dividera med 100 med ett rupietecken | gateway formatMinor | en skuld på ¥6,000 skrevs ut som -₹60.00 | symbol och decimaler från valutan |
| Saldon per valuta tillplattade till en rad | /v1/claims | vilken valuta kartan än skrev sist | en post per valuta, matchande GET /balances |
| Posteringar summerade per medlem, valuta bortfallen | ände-till-ände-liggarverifierare | en dubbelt trasig grupp rapporterad som balanserad | gruppera 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.
Populära inlägg
- Journalen som håller delade utgifter exakta8 min läsning
- Varför en ändrad utgift måste ange delningen på nytt8 min läsning
- Gör upp: reglera gruppens utgifter med färre överföringar5 min läsning
- Dela notan när en rätt inte delades9 min läsning
- Dela hyran rättvist med rumskompisar5 min läsning