Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Update TimeKeyEncoder to Preserve Lexicographical Order #21

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
cosmossdk.io/store v1.1.0
github.com/cosmos/cosmos-db v1.0.2
github.com/cosmos/cosmos-sdk v0.50.6
github.com/cosmos/gogoproto v1.4.12
github.com/gogo/protobuf v1.3.3
github.com/stretchr/testify v1.9.0
)
Expand Down Expand Up @@ -43,7 +44,6 @@ require (
github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect
github.com/cosmos/go-bip39 v1.0.0 // indirect
github.com/cosmos/gogogateway v1.2.0 // indirect
github.com/cosmos/gogoproto v1.4.12 // indirect
github.com/cosmos/iavl v1.1.2 // indirect
github.com/cosmos/ics23/go v0.10.0 // indirect
github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect
Expand Down
17 changes: 12 additions & 5 deletions keys.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package collections

import (
"encoding/binary"
"fmt"
"strconv"
"time"
Expand Down Expand Up @@ -59,13 +60,19 @@ func (uint64Key) Decode(b []byte) (int, uint64) { return 8, sdk.BigEndianToUint6
type timeKey struct{}

func (timeKey) Stringify(t time.Time) string { return t.String() }
func (timeKey) Encode(t time.Time) []byte { return sdk.FormatTimeBytes(t) }

func (timeKey) Encode(t time.Time) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(t.UnixNano()))
return b
}
Comment on lines +64 to +68
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Time range limitation needs documentation and testing

The implementation's use of UnixNano() introduces time range constraints (years 1678 to 2262), but this limitation is neither documented nor tested. Consider:

  • Add documentation in keys.go explaining the valid time range
  • Add test cases with edge timestamps to verify behavior near the bounds
  • Consider using a different encoding scheme if wider time range support is required
🔗 Analysis chain

Implementation ensures lexicographical ordering but has time range limitations.

The new implementation correctly uses binary encoding of Unix nanoseconds to ensure lexicographical ordering matches chronological ordering. However, there are some important considerations:

  1. This is a breaking change as existing encoded keys will not be decodable.
  2. The use of UnixNano() limits the valid time range to years 1678 to 2262 due to the int64 constraints.

Let's verify the time range limitation:


The search results show that all tests use timestamps from 2021, with relatively small time offsets (hours/seconds). There's no evidence of testing the time range limitations. Let's search for any documentation or comments about time range constraints.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any time values or tests that might be affected by the year 2262 limitation
rg -i "time\.(date|parse|unix)" -A 5

Length of output: 3537


Script:

#!/bin/bash
# Search for any documentation or comments about time constraints
rg -i "(unix|time).*(limit|constraint|range|bound)" 

Length of output: 117


func (timeKey) Decode(b []byte) (int, time.Time) {
t, err := sdk.ParseTimeBytes(b)
if err != nil {
panic(fmt.Errorf("%w %s", err, HumanizeBytes(b)))
if len(b) < 8 {
panic("invalid time key")
}
return len(b), t
ts := int64(binary.BigEndian.Uint64(b[:8]))
return 8, time.Unix(0, ts).UTC()
}

type accAddressKey struct{}
Expand Down
220 changes: 220 additions & 0 deletions keyset_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package collections

import (
"sort"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -53,3 +55,221 @@ func TestKeysetIterator(t *testing.T) {
iter.Next()
assert.False(t, iter.Valid())
}

func TestTimeKeySet(t *testing.T) {
sk, ctx, _ := deps()
keyset := NewKeySet[time.Time](sk, 0, TimeKeyEncoder)

// Use a fixed time
now := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
keyset.Insert(ctx, now)
require.True(t, keyset.Has(ctx, now))

// Test delete and get
keyset.Delete(ctx, now)
require.False(t, keyset.Has(ctx, now))
}

func TestTimeKeySet_IterateAscending(t *testing.T) {
sk, ctx, _ := deps()
keyset := NewKeySet[time.Time](sk, 0, TimeKeyEncoder)

now := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
times := []time.Time{
now.Add(2 * time.Second),
now.Add(1 * time.Second),
now.Add(3 * time.Second),
now,
}

// Insert times into the keyset
for _, t := range times {
keyset.Insert(ctx, t)
}

// Sort times in ascending order
sort.Slice(times, func(i, j int) bool {
return times[i].Before(times[j])
})

Comment on lines +91 to +94
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor duplicate sorting logic into helper functions

The sorting logic for time slices is repeated across multiple test functions. Refactoring this code into helper functions will reduce duplication and enhance maintainability.

Apply this refactor by adding helper functions:

// Helper function to sort times in ascending order
func sortTimesAsc(times []time.Time) []time.Time {
	sortedTimes := make([]time.Time, len(times))
	copy(sortedTimes, times)
	sort.Slice(sortedTimes, func(i, j int) bool {
		return sortedTimes[i].Before(sortedTimes[j])
	})
	return sortedTimes
}

// Helper function to sort times in descending order
func sortTimesDesc(times []time.Time) []time.Time {
	sortedTimes := make([]time.Time, len(times))
	copy(sortedTimes, times)
	sort.Slice(sortedTimes, func(i, j int) bool {
		return sortedTimes[i].After(sortedTimes[j])
	})
	return sortedTimes
}

Then, replace the sorting code in your test functions with these helper functions:

-// Sort times in ascending order
-sortedTimesAsc := make([]time.Time, len(times))
-copy(sortedTimesAsc, times)
-sort.Slice(sortedTimesAsc, func(i, j int) bool {
-	return sortedTimesAsc[i].Before(sortedTimesAsc[j])
-})
+sortedTimesAsc := sortTimesAsc(times)
-// Sort times in descending order
-sortedTimesDesc := make([]time.Time, len(times))
-copy(sortedTimesDesc, times)
-sort.Slice(sortedTimesDesc, func(i, j int) bool {
-	return sortedTimesDesc[i].After(sortedTimesDesc[j])
-})
+sortedTimesDesc := sortTimesDesc(times)

Also applies to: 130-133, 176-181, 196-201

// Iterate over the keyset in ascending order
iter := keyset.Iterate(ctx, Range[time.Time]{})
defer iter.Close()

keys := iter.Keys()
require.Equal(t, len(times), len(keys))

for i, k := range keys {
// Strip monotonic clock readings
expectedTime := times[i].Round(0)
actualTime := k.Round(0)

// Compare UnixNano timestamps
require.Equal(t, expectedTime.UnixNano(), actualTime.UnixNano())
}
}

func TestTimeKeySet_IterateDescending(t *testing.T) {
sk, ctx, _ := deps()
keyset := NewKeySet[time.Time](sk, 0, TimeKeyEncoder)

now := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
times := []time.Time{
now.Add(2 * time.Second),
now.Add(1 * time.Second),
now.Add(3 * time.Second),
now,
}

// Insert times into the keyset
for _, t := range times {
keyset.Insert(ctx, t)
}

// Sort times in descending order
sort.Slice(times, func(i, j int) bool {
return times[i].After(times[j])
})

// Iterate over the keyset in descending order
iter := keyset.Iterate(ctx, Range[time.Time]{}.Descending())
defer iter.Close()

keys := iter.Keys()
require.Equal(t, len(times), len(keys))

for i, k := range keys {
expectedTime := times[i].Round(0)
actualTime := k.Round(0)
require.Equal(t, expectedTime.UnixNano(), actualTime.UnixNano())
}
}

func TestTimeKeyEncoder_EncodeDecode(t *testing.T) {
now := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
encoded := TimeKeyEncoder.Encode(now)
_, decoded := TimeKeyEncoder.Decode(encoded)

// Compare UnixNano timestamps
require.Equal(t, now.UnixNano(), decoded.UnixNano())
}

func TestTimeKeySet_OrderConsistency(t *testing.T) {
sk, ctx, _ := deps()
keyset := NewKeySet[time.Time](sk, 0, TimeKeyEncoder)

now := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
times := []time.Time{
now.Add(-1 * time.Hour),
now,
now.Add(1 * time.Hour),
now.Add(2 * time.Hour),
now.Add(-2 * time.Hour),
}

// Insert times into the keyset
for _, t := range times {
keyset.Insert(ctx, t)
}

// Sort times in ascending order
sortedTimesAsc := make([]time.Time, len(times))
copy(sortedTimesAsc, times)
sort.Slice(sortedTimesAsc, func(i, j int) bool {
return sortedTimesAsc[i].Before(sortedTimesAsc[j])
})

// Iterate over the keyset in ascending order
iterAsc := keyset.Iterate(ctx, Range[time.Time]{})
defer iterAsc.Close()

keysAsc := iterAsc.Keys()
require.Equal(t, len(times), len(keysAsc))

for i, k := range keysAsc {
expectedTime := sortedTimesAsc[i].Round(0)
actualTime := k.Round(0)
require.Equal(t, expectedTime.UnixNano(), actualTime.UnixNano())
}

// Sort times in descending order
sortedTimesDesc := make([]time.Time, len(times))
copy(sortedTimesDesc, times)
sort.Slice(sortedTimesDesc, func(i, j int) bool {
return sortedTimesDesc[i].After(sortedTimesDesc[j])
})

// Iterate over the keyset in descending order
iterDesc := keyset.Iterate(ctx, Range[time.Time]{}.Descending())
defer iterDesc.Close()

keysDesc := iterDesc.Keys()
require.Equal(t, len(times), len(keysDesc))

for i, k := range keysDesc {
expectedTime := sortedTimesDesc[i].Round(0)
actualTime := k.Round(0)
require.Equal(t, expectedTime.UnixNano(), actualTime.UnixNano())
}
}

func TestTimeKeySet_IterateRange(t *testing.T) {
sk, ctx, _ := deps()
keyset := NewKeySet[time.Time](sk, 0, TimeKeyEncoder)

now := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
times := []time.Time{
now.Add(1 * time.Second),
now.Add(2 * time.Second),
now.Add(3 * time.Second),
now.Add(4 * time.Second),
now.Add(5 * time.Second),
}

// Insert times into the keyset
for _, t := range times {
keyset.Insert(ctx, t)
}

// Define range from now.Add(2s) inclusive to now.Add(4s) exclusive
iter := keyset.Iterate(ctx, Range[time.Time]{}.
StartInclusive(now.Add(2*time.Second)).
EndExclusive(now.Add(4*time.Second)))
defer iter.Close()

expectedTimes := []time.Time{
now.Add(2 * time.Second),
now.Add(3 * time.Second),
}

keys := iter.Keys()
require.Equal(t, len(expectedTimes), len(keys))

for i, k := range keys {
expectedTime := expectedTimes[i].Round(0)
actualTime := k.Round(0)
require.Equal(t, expectedTime.UnixNano(), actualTime.UnixNano())
}
}

func TestTimeKeySet_SameTimeKeys(t *testing.T) {
sk, ctx, _ := deps()
keyset := NewKeySet[time.Time](sk, 0, TimeKeyEncoder)

now := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)

// Insert the same time multiple times (should only be stored once in a set)
keyset.Insert(ctx, now)
keyset.Insert(ctx, now)
keyset.Insert(ctx, now)

iter := keyset.Iterate(ctx, Range[time.Time]{})
defer iter.Close()

keys := iter.Keys()
require.Equal(t, 1, len(keys))

expectedTime := now.Round(0)
actualTime := keys[0].Round(0)
require.Equal(t, expectedTime.UnixNano(), actualTime.UnixNano())
}
Loading