Skip to content

Commit 475bbd9

Browse files
Fix VerifyEvents to handle amounts exceeding int64 range (#5932)
* Fix VerifyEvents to handle amounts exceeding int64 range Replace int64 balance tracking with *big.Int in findBalanceDeltasFromEvents and all related functions. This fixes the unrecovered panic in MustParseInt64Raw when processing SAC events with i128 amounts that exceed int64 range (e.g., cumulative mints producing balances >2^63-1). findBalanceDeltasFromEvents now returns an error instead of panicking on unparseable amounts. No exported API signatures changed. Fixes #5929 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address review feedback: fix zero-delta insertion and hoist bigIntComparer - Short-circuit updateBalanceMap when delta is zero to prevent inserting spurious zero-valued map entries (could cause false verify mismatches) - Move bigIntComparer allocation outside the transaction loop in VerifyEvents since it's stateless and reusable - Add test for zero-delta edge case Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix NewMaxFee() truncating int64 fee-bump fee to uint32 FeeBumpTransaction.Fee is XDR Int64 but NewMaxFee() was casting it to uint32, silently discarding the upper 32 bits. Any fee-bump fee above ~429.5 XLM (4,294,967,295 stroops) returned a wrong value. Change return type from uint32 to int64 — this is a breaking change. Fixes #5931 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a445f0c commit 475bbd9

5 files changed

Lines changed: 290 additions & 44 deletions

File tree

ingest/ledger_transaction.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -736,12 +736,12 @@ func (t *LedgerTransaction) InnerTransactionHash() (string, bool) {
736736
return hex.EncodeToString(innerHash[:]), true
737737
}
738738

739-
func (t *LedgerTransaction) NewMaxFee() (uint32, bool) {
739+
func (t *LedgerTransaction) NewMaxFee() (int64, bool) {
740740
if !t.Envelope.IsFeeBump() {
741741
return 0, false
742742
}
743743

744-
return uint32(t.Envelope.FeeBumpFee()), true
744+
return t.Envelope.FeeBumpFee(), true
745745
}
746746

747747
func (t *LedgerTransaction) Successful() bool {

ingest/ledger_transaction_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -758,10 +758,10 @@ func TestTransactionHelperFunctions(t *testing.T) {
758758
assert.Equal(t, false, ok)
759759
assert.Equal(t, "", innerTransactionHash)
760760

761-
var newMaxFee uint32
761+
var newMaxFee int64
762762
newMaxFee, ok = transaction.NewMaxFee()
763763
assert.Equal(t, false, ok)
764-
assert.Equal(t, uint32(0), newMaxFee)
764+
assert.Equal(t, int64(0), newMaxFee)
765765

766766
assert.Equal(t, true, transaction.Successful())
767767
}

processors/token_transfer/token_transfer_processor.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"io"
7+
"math/big"
78

89
"github.com/stellar/go-stellar-sdk/amount"
910
assetProto "github.com/stellar/go-stellar-sdk/asset"
@@ -993,14 +994,20 @@ For more details, on why this is needed, refer - https://github.com/stellar/stel
993994
994995
The maybeGenerateMintOrBurnEvents function takes in an account and an asset, but in reality, this will only be called for operationSourceAccount and strictly for XLM
995996
*/
996-
func (p *EventsProcessor) maybeGenerateMintOrBurnEventsForReconciliation(tx ingest.LedgerTransaction, opIndex uint32, changesMap, eventsMap map[balanceKey]int64, account xdr.MuxedAccount, asset xdr.Asset) (*TokenTransferEvent, error) {
997+
func (p *EventsProcessor) maybeGenerateMintOrBurnEventsForReconciliation(tx ingest.LedgerTransaction, opIndex uint32, changesMap, eventsMap map[balanceKey]*big.Int, account xdr.MuxedAccount, asset xdr.Asset) (*TokenTransferEvent, error) {
997998
accountStr := account.ToAccountId().Address()
998999
// Create the balance key for this account and XLM asset
9991000
key := balanceKey{holder: accountStr, asset: asset.StringCanonical()}
10001001

1001-
// Get the balance changes from both maps
1002+
// Get the balance changes from both maps (nil means zero)
10021003
changesBalance := changesMap[key]
1004+
if changesBalance == nil {
1005+
changesBalance = new(big.Int)
1006+
}
10031007
eventsBalance := eventsMap[key]
1008+
if eventsBalance == nil {
1009+
eventsBalance = new(big.Int)
1010+
}
10041011

10051012
/*
10061013
Highlighting all possible scenarios:
@@ -1053,11 +1060,10 @@ func (p *EventsProcessor) maybeGenerateMintOrBurnEventsForReconciliation(tx inge
10531060
*/
10541061

10551062
// Both maps have entries for this account/asset
1056-
diff := changesBalance - eventsBalance
1057-
// Not in either map, no difference
1063+
diff := new(big.Int).Sub(changesBalance, eventsBalance)
10581064

10591065
// If no difference, no mint or burn needs to be emitted
1060-
if diff == 0 {
1066+
if diff.Sign() == 0 {
10611067
return nil, nil
10621068
}
10631069

@@ -1067,12 +1073,12 @@ func (p *EventsProcessor) maybeGenerateMintOrBurnEventsForReconciliation(tx inge
10671073
protoAsset := assetProto.NewProtoAsset(asset)
10681074

10691075
// Generate appropriate event based on the difference
1070-
if diff > 0 {
1076+
if diff.Sign() > 0 {
10711077
// changesMap shows more XLM than eventsMap - need to MINT
1072-
mintOrBurnEvent = NewMintEvent(meta, accountStr, amount.String64Raw(xdr.Int64(diff)), protoAsset)
1078+
mintOrBurnEvent = NewMintEvent(meta, accountStr, diff.String(), protoAsset)
10731079
} else {
10741080
// changesMap shows less XLM than eventsMap - need to BURN
1075-
mintOrBurnEvent = NewBurnEvent(meta, accountStr, amount.String64Raw(xdr.Int64(-diff)), protoAsset)
1081+
mintOrBurnEvent = NewBurnEvent(meta, accountStr, new(big.Int).Abs(diff).String(), protoAsset)
10761082
}
10771083

10781084
return mintOrBurnEvent, nil
@@ -1085,7 +1091,10 @@ func (p *EventsProcessor) generateXlmReconciliationEvents(tx ingest.LedgerTransa
10851091
return nil, fmt.Errorf("failed to get operation changes for operation Index: %v: %w", opIndex, err)
10861092
}
10871093
changesMap := findBalanceDeltasFromChanges(operationChanges)
1088-
eventsMap := findBalanceDeltasFromEvents(operationEvents)
1094+
eventsMap, err := findBalanceDeltasFromEvents(operationEvents)
1095+
if err != nil {
1096+
return nil, fmt.Errorf("failed to compute event balance deltas for operation Index: %v: %w", opIndex, err)
1097+
}
10891098
operationSrcAccount := operationSourceAccount(tx, op)
10901099

10911100
return p.maybeGenerateMintOrBurnEventsForReconciliation(tx, opIndex, changesMap, eventsMap, operationSrcAccount, xlmAsset)

processors/token_transfer/verify_events.go

Lines changed: 72 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package token_transfer
33
import (
44
"fmt"
55
"io"
6+
"math/big"
67

78
"github.com/google/go-cmp/cmp"
8-
"github.com/stellar/go-stellar-sdk/amount"
99
"github.com/stellar/go-stellar-sdk/ingest"
1010
"github.com/stellar/go-stellar-sdk/strkey"
1111
"github.com/stellar/go-stellar-sdk/xdr"
@@ -18,18 +18,25 @@ type balanceKey struct {
1818
}
1919

2020
// updateBalanceMap updates the map and removes the entry if the value becomes 0
21-
func updateBalanceMap(m map[balanceKey]int64, key balanceKey, delta int64) {
21+
func updateBalanceMap(m map[balanceKey]*big.Int, key balanceKey, delta *big.Int) {
2222
// We dont include movement to/from contract address is balance delta tracking, since there is no standard way to derive/verify from contractData
2323
if strkey.IsValidContractAddress(key.holder) {
2424
return
2525
}
26-
m[key] += delta
27-
if m[key] == 0 {
28-
delete(m, key)
26+
if delta.Sign() == 0 {
27+
return
28+
}
29+
if existing, ok := m[key]; ok {
30+
existing.Add(existing, delta)
31+
if existing.Sign() == 0 {
32+
delete(m, key)
33+
}
34+
} else {
35+
m[key] = new(big.Int).Set(delta)
2936
}
3037
}
3138

32-
func fetchAccountDeltaFromChange(change ingest.Change, m map[balanceKey]int64) {
39+
func fetchAccountDeltaFromChange(change ingest.Change, m map[balanceKey]*big.Int) {
3340
var accountKey string
3441
var pre, post xdr.Int64
3542

@@ -44,11 +51,11 @@ func fetchAccountDeltaFromChange(change ingest.Change, m map[balanceKey]int64) {
4451
post = entry.Balance
4552
}
4653

47-
delta := int64(post - pre)
54+
delta := big.NewInt(int64(post - pre))
4855
updateBalanceMap(m, balanceKey{holder: accountKey, asset: xlmAsset.StringCanonical()}, delta)
4956
}
5057

51-
func fetchTrustlineDeltaFromChange(change ingest.Change, m map[balanceKey]int64) {
58+
func fetchTrustlineDeltaFromChange(change ingest.Change, m map[balanceKey]*big.Int) {
5259
var trustlineKey string
5360
var asset string
5461
var pre, post xdr.Int64
@@ -72,11 +79,11 @@ func fetchTrustlineDeltaFromChange(change ingest.Change, m map[balanceKey]int64)
7279
asset = entry.Asset.ToAsset().StringCanonical()
7380
}
7481

75-
delta := int64(post - pre)
82+
delta := big.NewInt(int64(post - pre))
7683
updateBalanceMap(m, balanceKey{holder: trustlineKey, asset: asset}, delta)
7784
}
7885

79-
func fetchClaimableDeltaFromChange(change ingest.Change, m map[balanceKey]int64) {
86+
func fetchClaimableDeltaFromChange(change ingest.Change, m map[balanceKey]*big.Int) {
8087
var cbKey string
8188
var asset string
8289
var pre, post xdr.Int64
@@ -94,11 +101,11 @@ func fetchClaimableDeltaFromChange(change ingest.Change, m map[balanceKey]int64)
94101
post = entry.Amount
95102
}
96103

97-
delta := int64(post - pre)
104+
delta := big.NewInt(int64(post - pre))
98105
updateBalanceMap(m, balanceKey{holder: cbKey, asset: asset}, delta)
99106
}
100107

101-
func fetchLiquidityPoolDeltaFromChange(change ingest.Change, m map[balanceKey]int64) {
108+
func fetchLiquidityPoolDeltaFromChange(change ingest.Change, m map[balanceKey]*big.Int) {
102109
var lpKey string
103110
var assetA, assetB string
104111
var preA, preB, postA, postB xdr.Int64
@@ -119,16 +126,16 @@ func fetchLiquidityPoolDeltaFromChange(change ingest.Change, m map[balanceKey]in
119126
postA, postB = cp.ReserveA, cp.ReserveB
120127
}
121128

122-
deltaA := int64(postA - preA)
123-
deltaB := int64(postB - preB)
129+
deltaA := big.NewInt(int64(postA - preA))
130+
deltaB := big.NewInt(int64(postB - preB))
124131

125132
updateBalanceMap(m, balanceKey{holder: lpKey, asset: assetA}, deltaA)
126133
updateBalanceMap(m, balanceKey{holder: lpKey, asset: assetB}, deltaB)
127134
}
128135

129136
// findBalanceDeltasFromChanges aggregates all balance changes from ledger entry changes
130-
func findBalanceDeltasFromChanges(changes []ingest.Change) map[balanceKey]int64 {
131-
hashmap := make(map[balanceKey]int64)
137+
func findBalanceDeltasFromChanges(changes []ingest.Change) map[balanceKey]*big.Int {
138+
hashmap := make(map[balanceKey]*big.Int)
132139
for _, change := range changes {
133140
switch change.Type {
134141
case xdr.LedgerEntryTypeAccount:
@@ -144,9 +151,18 @@ func findBalanceDeltasFromChanges(changes []ingest.Change) map[balanceKey]int64
144151
return hashmap
145152
}
146153

154+
// parseAmount parses a raw amount string into a *big.Int.
155+
func parseAmount(amountStr string, eventType string) (*big.Int, error) {
156+
amt, ok := new(big.Int).SetString(amountStr, 10)
157+
if !ok {
158+
return nil, fmt.Errorf("invalid amount %q in %s event", amountStr, eventType)
159+
}
160+
return amt, nil
161+
}
162+
147163
// findBalanceDeltasFromEvents aggregates all balance changes from token transfer events
148-
func findBalanceDeltasFromEvents(events []*TokenTransferEvent) map[balanceKey]int64 {
149-
hashmap := make(map[balanceKey]int64)
164+
func findBalanceDeltasFromEvents(events []*TokenTransferEvent) (map[balanceKey]*big.Int, error) {
165+
hashmap := make(map[balanceKey]*big.Int)
150166

151167
for _, event := range events {
152168
if event.GetAsset() == nil { // needed check for custom token events which won't have an asset
@@ -158,50 +174,65 @@ func findBalanceDeltasFromEvents(events []*TokenTransferEvent) map[balanceKey]in
158174
ev := event.GetFee()
159175
address := ev.From
160176
asset := xlmAsset.StringCanonical()
161-
amt := amount.MustParseInt64Raw(ev.Amount)
177+
amt, err := parseAmount(ev.Amount, string(event.GetEventType()))
178+
if err != nil {
179+
return nil, err
180+
}
162181
// Address' balance reduces by amt in FEE
163-
updateBalanceMap(hashmap, balanceKey{holder: address, asset: asset}, -amt)
182+
updateBalanceMap(hashmap, balanceKey{holder: address, asset: asset}, new(big.Int).Neg(amt))
164183

165184
case *TokenTransferEvent_Transfer:
166185
ev := event.GetTransfer()
167186
fromAddress := ev.From
168187
toAddress := ev.To
169-
amt := amount.MustParseInt64Raw(ev.Amount)
188+
amt, err := parseAmount(ev.Amount, string(event.GetEventType()))
189+
if err != nil {
190+
return nil, err
191+
}
170192
asset := event.GetAsset().ToXdrAsset().StringCanonical()
171193
// FromAddress' balance reduces by amt in TRANSFER
172-
updateBalanceMap(hashmap, balanceKey{holder: fromAddress, asset: asset}, -amt)
194+
updateBalanceMap(hashmap, balanceKey{holder: fromAddress, asset: asset}, new(big.Int).Neg(amt))
173195
// ToAddress' balance increases by amt in TRANSFER
174196
updateBalanceMap(hashmap, balanceKey{holder: toAddress, asset: asset}, amt)
175197

176198
case *TokenTransferEvent_Mint:
177199
ev := event.GetMint()
178200
toAddress := ev.To
179201
asset := event.GetAsset().ToXdrAsset().StringCanonical()
180-
amt := amount.MustParseInt64Raw(ev.Amount)
202+
amt, err := parseAmount(ev.Amount, string(event.GetEventType()))
203+
if err != nil {
204+
return nil, err
205+
}
181206
// ToAddress' balance increases by amt in MINT
182207
updateBalanceMap(hashmap, balanceKey{holder: toAddress, asset: asset}, amt)
183208

184209
case *TokenTransferEvent_Burn:
185210
ev := event.GetBurn()
186211
fromAddress := ev.From
187212
asset := event.GetAsset().ToXdrAsset().StringCanonical()
188-
amt := amount.MustParseInt64Raw(ev.Amount)
213+
amt, err := parseAmount(ev.Amount, string(event.GetEventType()))
214+
if err != nil {
215+
return nil, err
216+
}
189217
// FromAddress' balance reduces by amt in BURN
190-
updateBalanceMap(hashmap, balanceKey{holder: fromAddress, asset: asset}, -amt)
218+
updateBalanceMap(hashmap, balanceKey{holder: fromAddress, asset: asset}, new(big.Int).Neg(amt))
191219

192220
case *TokenTransferEvent_Clawback:
193221
ev := event.GetClawback()
194222
fromAddress := ev.From
195223
asset := event.GetAsset().ToXdrAsset().StringCanonical()
196-
amt := amount.MustParseInt64Raw(ev.Amount)
224+
amt, err := parseAmount(ev.Amount, string(event.GetEventType()))
225+
if err != nil {
226+
return nil, err
227+
}
197228
// FromAddress' balance reduces by amt in CLAWBACK
198-
updateBalanceMap(hashmap, balanceKey{holder: fromAddress, asset: asset}, -amt)
229+
updateBalanceMap(hashmap, balanceKey{holder: fromAddress, asset: asset}, new(big.Int).Neg(amt))
199230

200231
default:
201-
panic(fmt.Errorf("unknown event type %s", event.GetEventType()))
232+
return nil, fmt.Errorf("unknown event type %s", event.GetEventType())
202233
}
203234
}
204-
return hashmap
235+
return hashmap, nil
205236
}
206237

207238
func VerifyEvents(ledger xdr.LedgerCloseMeta, passphrase string, readFromUnifiedEvents bool) error {
@@ -216,6 +247,13 @@ func VerifyEvents(ledger xdr.LedgerCloseMeta, passphrase string, readFromUnified
216247
return fmt.Errorf("error creating transaction reader: %w", err)
217248
}
218249

250+
bigIntComparer := cmp.Comparer(func(a, b *big.Int) bool {
251+
if a == nil || b == nil {
252+
return a == b
253+
}
254+
return a.Cmp(b) == 0
255+
})
256+
219257
for {
220258
var tx ingest.LedgerTransaction
221259
var events []*TokenTransferEvent
@@ -246,10 +284,13 @@ func VerifyEvents(ledger xdr.LedgerCloseMeta, passphrase string, readFromUnified
246284
changes := append(feeChanges, txChanges...)
247285
changes = append(changes, postTxApplyFeeChanges...)
248286

249-
txEventsMap := findBalanceDeltasFromEvents(events)
287+
txEventsMap, err := findBalanceDeltasFromEvents(events)
288+
if err != nil {
289+
return fmt.Errorf("verifyEventsError: %w", err)
290+
}
250291
txChangesMap := findBalanceDeltasFromChanges(changes)
251292

252-
if diff := cmp.Diff(txEventsMap, txChangesMap); diff != "" {
293+
if diff := cmp.Diff(txEventsMap, txChangesMap, bigIntComparer); diff != "" {
253294
return fmt.Errorf("balance delta mismatch between events and ledger changes for ledgerSequence: %v, closedAt: %v, txHash: %v\n"+
254295
"('-' indicates missing or different in events, '+' indicates missing or different in ledger changes)\n%s", ledger.LedgerSequence(), ledger.ClosedAt(), txHash, diff)
255296
}

0 commit comments

Comments
 (0)