Skip to content

Commit da57cbb

Browse files
authored
fix(ledger): use big.Int for era history to prevent overflow (#1421)
Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent 461e306 commit da57cbb

File tree

2 files changed

+247
-5
lines changed

2 files changed

+247
-5
lines changed

ledger/queries.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package ledger
1717
import (
1818
"errors"
1919
"fmt"
20+
"math"
2021
"math/big"
2122

2223
"github.com/blinklabs-io/dingo/database/models"
@@ -103,6 +104,38 @@ func (ls *LedgerState) queryHardFork(
103104
}
104105
}
105106

107+
// epochPicoseconds computes the duration of an epoch
108+
// in picoseconds: slotLength * lengthInSlots * 1e9.
109+
// It uses big.Int arithmetic to prevent overflow when
110+
// the uint product would exceed math.MaxUint64.
111+
func epochPicoseconds(
112+
slotLength, lengthInSlots uint,
113+
) *big.Int {
114+
result := new(big.Int).SetUint64(uint64(slotLength))
115+
result.Mul(
116+
result,
117+
new(big.Int).SetUint64(uint64(lengthInSlots)),
118+
)
119+
result.Mul(result, big.NewInt(1_000_000_000))
120+
return result
121+
}
122+
123+
// checkedSlotAdd adds startSlot + length with overflow
124+
// detection. Returns an error if the result would
125+
// exceed math.MaxUint64.
126+
func checkedSlotAdd(
127+
startSlot, length uint64,
128+
) (uint64, error) {
129+
if startSlot > math.MaxUint64-length {
130+
return 0, fmt.Errorf(
131+
"era history overflow: start slot %d + length %d",
132+
startSlot,
133+
length,
134+
)
135+
}
136+
return startSlot + length, nil
137+
}
138+
106139
func (ls *LedgerState) queryHardForkEraHistory() (any, error) {
107140
retData := []any{}
108141
timespan := big.NewInt(0)
@@ -149,17 +182,23 @@ func (ls *LedgerState) queryHardForkEraHistory() (any, error) {
149182
// Add epoch length in picoseconds to timespan
150183
timespan.Add(
151184
timespan,
152-
new(big.Int).SetUint64(
153-
uint64(
154-
tmpEpoch.SlotLength*tmpEpoch.LengthInSlots*1_000_000_000,
155-
),
185+
epochPicoseconds(
186+
tmpEpoch.SlotLength,
187+
tmpEpoch.LengthInSlots,
156188
),
157189
)
158190
// Update era end
159191
if idx == len(epochs)-1 {
192+
endSlot, slotErr := checkedSlotAdd(
193+
tmpEpoch.StartSlot,
194+
uint64(tmpEpoch.LengthInSlots),
195+
)
196+
if slotErr != nil {
197+
return nil, slotErr
198+
}
160199
tmpEnd = []any{
161200
new(big.Int).Set(timespan),
162-
tmpEpoch.StartSlot + uint64(tmpEpoch.LengthInSlots),
201+
endSlot,
163202
tmpEpoch.EpochId + 1,
164203
}
165204
}

ledger/queries_test.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
package ledger
1616

1717
import (
18+
"math"
19+
"math/big"
1820
"testing"
1921

2022
"github.com/blinklabs-io/gouroboros/ledger"
2123
olocalstatequery "github.com/blinklabs-io/gouroboros/protocol/localstatequery"
24+
"github.com/stretchr/testify/assert"
2225
"github.com/stretchr/testify/require"
2326
)
2427

@@ -47,3 +50,203 @@ func TestQueryShelleyUtxoByTxIn_EmptySlice(t *testing.T) {
4750
require.True(t, ok, "expected UtxoId map")
4851
require.Empty(t, m)
4952
}
53+
54+
func TestEpochPicoseconds(t *testing.T) {
55+
tests := []struct {
56+
name string
57+
slotLength uint
58+
lengthInSlots uint
59+
expected *big.Int
60+
}{
61+
{
62+
// Shelley epoch: 1000ms slots, 432000 slots
63+
// 1000 * 432000 * 1e9 = 432_000_000_000_000_000
64+
name: "shelley epoch",
65+
slotLength: 1000,
66+
lengthInSlots: 432000,
67+
expected: new(big.Int).SetUint64(
68+
432_000_000_000_000_000,
69+
),
70+
},
71+
{
72+
// Byron epoch: 20000ms slots, 21600 slots
73+
// 20000 * 21600 * 1e9 = 432_000_000_000_000_000
74+
name: "byron epoch",
75+
slotLength: 20000,
76+
lengthInSlots: 21600,
77+
expected: new(big.Int).SetUint64(
78+
432_000_000_000_000_000,
79+
),
80+
},
81+
{
82+
name: "zero slot length",
83+
slotLength: 0,
84+
lengthInSlots: 432000,
85+
expected: big.NewInt(0),
86+
},
87+
{
88+
name: "zero length in slots",
89+
slotLength: 1000,
90+
lengthInSlots: 0,
91+
expected: big.NewInt(0),
92+
},
93+
{
94+
// Large values that would overflow uint64 in
95+
// naive uint multiplication:
96+
// MaxUint32 * MaxUint32 * 1e9 overflows uint64,
97+
// but big.Int handles it correctly.
98+
name: "large values no overflow",
99+
slotLength: math.MaxUint32,
100+
lengthInSlots: math.MaxUint32,
101+
expected: func() *big.Int {
102+
a := new(big.Int).SetUint64(math.MaxUint32)
103+
b := new(big.Int).SetUint64(math.MaxUint32)
104+
r := new(big.Int).Mul(a, b)
105+
r.Mul(r, big.NewInt(1_000_000_000))
106+
return r
107+
}(),
108+
},
109+
{
110+
name: "single slot single ms",
111+
slotLength: 1,
112+
lengthInSlots: 1,
113+
expected: big.NewInt(1_000_000_000),
114+
},
115+
}
116+
for _, tc := range tests {
117+
t.Run(tc.name, func(t *testing.T) {
118+
result := epochPicoseconds(
119+
tc.slotLength,
120+
tc.lengthInSlots,
121+
)
122+
// Use Cmp instead of Equal because big.Int
123+
// internal representation of zero varies
124+
// (nil abs vs empty abs).
125+
assert.Equal(
126+
t,
127+
0,
128+
tc.expected.Cmp(result),
129+
"picosecond calculation mismatch: "+
130+
"expected %s, got %s",
131+
tc.expected.String(), result.String(),
132+
)
133+
})
134+
}
135+
}
136+
137+
func TestEpochPicoseconds_OverflowSafe(t *testing.T) {
138+
// Verify that large values that would overflow uint64
139+
// in naive multiplication are handled correctly by
140+
// big.Int arithmetic.
141+
//
142+
// MaxUint32 * MaxUint32 = 18446744065119617025
143+
// which is close to MaxUint64 (18446744073709551615).
144+
// Multiplying by 1e9 would massively overflow uint64.
145+
result := epochPicoseconds(
146+
math.MaxUint32,
147+
math.MaxUint32,
148+
)
149+
150+
// The result must be larger than MaxUint64
151+
maxU64 := new(big.Int).SetUint64(math.MaxUint64)
152+
assert.Equal(
153+
t,
154+
1,
155+
result.Cmp(maxU64),
156+
"result should exceed MaxUint64",
157+
)
158+
159+
// Verify the exact value:
160+
// MaxUint32^2 * 1e9 =
161+
// 4294967295 * 4294967295 * 1000000000 =
162+
// 18446744065119617025000000000
163+
expected, ok := new(big.Int).SetString(
164+
"18446744065119617025000000000",
165+
10,
166+
)
167+
require.True(t, ok)
168+
assert.Equal(
169+
t,
170+
0,
171+
expected.Cmp(result),
172+
"exact overflow value mismatch",
173+
)
174+
}
175+
176+
func TestCheckedSlotAdd(t *testing.T) {
177+
tests := []struct {
178+
name string
179+
startSlot uint64
180+
length uint64
181+
expected uint64
182+
expectErr bool
183+
}{
184+
{
185+
name: "normal addition",
186+
startSlot: 100,
187+
length: 200,
188+
expected: 300,
189+
},
190+
{
191+
name: "zero plus zero",
192+
startSlot: 0,
193+
length: 0,
194+
expected: 0,
195+
},
196+
{
197+
name: "zero plus value",
198+
startSlot: 0,
199+
length: 1000,
200+
expected: 1000,
201+
},
202+
{
203+
name: "max minus one plus one",
204+
startSlot: math.MaxUint64 - 1,
205+
length: 1,
206+
expected: math.MaxUint64,
207+
},
208+
{
209+
name: "max plus zero",
210+
startSlot: math.MaxUint64,
211+
length: 0,
212+
expected: math.MaxUint64,
213+
},
214+
{
215+
name: "overflow max plus one",
216+
startSlot: math.MaxUint64,
217+
length: 1,
218+
expectErr: true,
219+
},
220+
{
221+
name: "overflow large values",
222+
startSlot: math.MaxUint64 / 2,
223+
length: math.MaxUint64/2 + 2,
224+
expectErr: true,
225+
},
226+
{
227+
name: "realistic shelley epoch end",
228+
startSlot: 86400000,
229+
length: 432000,
230+
expected: 86832000,
231+
},
232+
}
233+
for _, tc := range tests {
234+
t.Run(tc.name, func(t *testing.T) {
235+
result, err := checkedSlotAdd(
236+
tc.startSlot,
237+
tc.length,
238+
)
239+
if tc.expectErr {
240+
require.Error(t, err)
241+
assert.Contains(
242+
t,
243+
err.Error(),
244+
"era history overflow",
245+
)
246+
return
247+
}
248+
require.NoError(t, err)
249+
assert.Equal(t, tc.expected, result)
250+
})
251+
}
252+
}

0 commit comments

Comments
 (0)