Skip to content
51 changes: 17 additions & 34 deletions contract/r/gnoswap/gov/staker/tree.gno
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@ import (
"strings"

avl "gno.land/p/nt/avl/v0"
ufmt "gno.land/p/nt/ufmt/v0"
)

// UintTree is a wrapper around an AVL tree for storing block timestamps as strings.
// Since block timestamps are defined as int64, we take int64 and convert it to uint64 for the tree.
// UintTree is a wrapper around an AVL tree for storing uint64 keys as strings.
//
// Methods:
// - Get: Retrieves a value associated with a uint64 key.
// - set: Stores a value with a uint64 key.
// - Set: Stores a value with a uint64 key.
// - Has: Checks if a uint64 key exists in the tree.
// - remove: Removes a uint64 key and its associated value.
// - Remove: Removes a uint64 key and its associated value.
// - Iterate: Iterates over keys and values in a range.
// - ReverseIterate: Iterates in reverse order over keys and values in a range.
type UintTree struct {
Expand All @@ -29,35 +27,35 @@ func NewUintTree() *UintTree {
}
}

func (self *UintTree) Get(key int64) (any, bool) {
v, ok := self.tree.Get(EncodeInt64(key))
func (self *UintTree) Get(key uint64) (any, bool) {
v, ok := self.tree.Get(EncodeUint(key))
if !ok {
return nil, false
}
return v, true
}

func (self *UintTree) Set(key int64, value any) {
self.tree.Set(EncodeInt64(key), value)
func (self *UintTree) Set(key uint64, value any) {
self.tree.Set(EncodeUint(key), value)
}

func (self *UintTree) Has(key int64) bool {
return self.tree.Has(EncodeInt64(key))
func (self *UintTree) Has(key uint64) bool {
return self.tree.Has(EncodeUint(key))
}

func (self *UintTree) Remove(key int64) {
self.tree.Remove(EncodeInt64(key))
func (self *UintTree) Remove(key uint64) {
self.tree.Remove(EncodeUint(key))
}

func (self *UintTree) Iterate(start, end int64, fn func(key int64, value any) bool) {
self.tree.Iterate(EncodeInt64(start), EncodeInt64(end), func(key string, value any) bool {
return fn(DecodeInt64(key), value)
func (self *UintTree) Iterate(start, end uint64, fn func(key uint64, value any) bool) {
self.tree.Iterate(EncodeUint(start), EncodeUint(end), func(key string, value any) bool {
return fn(DecodeUint(key), value)
})
}

func (self *UintTree) ReverseIterate(start, end int64, fn func(key int64, value any) bool) {
self.tree.ReverseIterate(EncodeInt64(start), EncodeInt64(end), func(key string, value any) bool {
return fn(DecodeInt64(key), value)
func (self *UintTree) ReverseIterate(start, end uint64, fn func(key uint64, value any) bool) {
self.tree.ReverseIterate(EncodeUint(start), EncodeUint(end), func(key string, value any) bool {
return fn(DecodeUint(key), value)
})
}

Expand Down Expand Up @@ -86,13 +84,6 @@ func EncodeUint(num uint64) string {
return strings.Repeat("0", zerosNeeded) + s
}

func EncodeInt64(num int64) string {
if num < 0 {
panic(ufmt.Sprintf("negative value not supported: %d", num))
}
return EncodeUint(uint64(num))
}

// DecodeUint converts a zero-padded string back into a uint64 number.
//
// Parameters:
Expand All @@ -114,11 +105,3 @@ func DecodeUint(s string) uint64 {
}
return num
}

func DecodeInt64(s string) int64 {
num, err := strconv.ParseInt(s, 10, 64)
if err != nil {
panic(err)
}
return num
}
10 changes: 8 additions & 2 deletions contract/r/gnoswap/gov/staker/v1/getter.gno
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ func (gs *govStakerV1) GetTotalDelegationAmountAtSnapshot(snapshotTime int64) (i
return 0, false
}

if snapshotTime < 0 {
panic("snapshotTime must be non-negative")
}
toTimestamp := snapshotTime
if toTimestamp < math.MaxInt64 {
toTimestamp = toTimestamp + 1
Expand All @@ -160,7 +163,7 @@ func (gs *govStakerV1) GetTotalDelegationAmountAtSnapshot(snapshotTime int64) (i
)

// ReverseIterate from 0 to snapshotTime to find the most recent entry at or before snapshotTime
history.ReverseIterate(0, toTimestamp, func(key int64, value any) bool {
history.ReverseIterate(0, uint64(toTimestamp), func(key uint64, value any) bool {
amountInt, ok := value.(int64)
if !ok {
panic(ufmt.Sprintf("invalid amount type: %T", value))
Expand Down Expand Up @@ -200,6 +203,9 @@ func (gs *govStakerV1) GetUserDelegationAmountAtSnapshot(userAddr address, snaps
return 0, false
}

if snapshotTime < 0 {
panic("snapshotTime must be non-negative")
}
toTimestamp := snapshotTime
if toTimestamp < math.MaxInt64 {
toTimestamp = toTimestamp + 1
Expand All @@ -211,7 +217,7 @@ func (gs *govStakerV1) GetUserDelegationAmountAtSnapshot(userAddr address, snaps
)

// ReverseIterate from 0 to snapshotTime to find the most recent entry at or before snapshotTime
userHistory.ReverseIterate(0, toTimestamp, func(key int64, value any) bool {
userHistory.ReverseIterate(0, uint64(toTimestamp), func(key uint64, value any) bool {
amountInt, ok := value.(int64)
if !ok {
panic(ufmt.Sprintf("invalid amount type: %T", value))
Expand Down
18 changes: 12 additions & 6 deletions contract/r/gnoswap/gov/staker/v1/staker_delegation_snapshot.gno
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ func (gs *govStakerV1) CleanStakerDelegationSnapshotByAdmin(snapshotTime int64)
// cleanTotalDelegationHistory removes total delegation history entries older than cutoff time.
// Keeps the most recent entry before cutoff to preserve state continuity.
func (gs *govStakerV1) cleanTotalDelegationHistory(cutoffTimestamp int64) {
if cutoffTimestamp < 0 {
panic("cutoffTimestamp must be non-negative")
}
history := gs.store.GetTotalDelegationHistory()

toTimestamp := cutoffTimestamp
Expand All @@ -106,7 +109,7 @@ func (gs *govStakerV1) cleanTotalDelegationHistory(cutoffTimestamp int64) {

hasLastValue := false

history.ReverseIterate(0, toTimestamp, func(timestamp int64, value any) bool {
history.ReverseIterate(0, uint64(toTimestamp), func(timestamp uint64, value any) bool {
lastValue = value
hasLastValue = true

Expand All @@ -115,10 +118,10 @@ func (gs *govStakerV1) cleanTotalDelegationHistory(cutoffTimestamp int64) {

// If there was a value before cutoff, set it at cutoff time to preserve continuity
if hasLastValue {
history.Set(cutoffTimestamp, lastValue)
history.Set(uint64(cutoffTimestamp), lastValue)
}

history.Iterate(0, cutoffTimestamp, func(timestamp int64, _ any) bool {
history.Iterate(0, uint64(cutoffTimestamp), func(timestamp uint64, _ any) bool {
history.Remove(timestamp)

return false // continue
Expand All @@ -133,6 +136,9 @@ func (gs *govStakerV1) cleanTotalDelegationHistory(cutoffTimestamp int64) {
// Structure: address -> *UintTree[timestamp -> int64]
// Keeps the most recent entry before cutoff for each user to preserve state continuity.
func (gs *govStakerV1) cleanUserDelegationHistory(cutoffTimestamp int64) {
if cutoffTimestamp < 0 {
panic("cutoffTimestamp must be non-negative")
}
history := gs.store.GetUserDelegationHistory()

// Iterate over all users and clean each user's history
Expand All @@ -149,7 +155,7 @@ func (gs *govStakerV1) cleanUserDelegationHistory(cutoffTimestamp int64) {

hasLastValue := false

userHistory.ReverseIterate(0, toTimestamp, func(_ int64, val any) bool {
userHistory.ReverseIterate(0, uint64(toTimestamp), func(_ uint64, val any) bool {
lastValue = val
hasLastValue = true

Expand All @@ -158,11 +164,11 @@ func (gs *govStakerV1) cleanUserDelegationHistory(cutoffTimestamp int64) {

// If there was a value before cutoff, set it at cutoff time to preserve continuity
if hasLastValue {
userHistory.Set(cutoffTimestamp, lastValue)
userHistory.Set(uint64(cutoffTimestamp), lastValue)
}

// Copy all entries at or after cutoff time
userHistory.Iterate(0, cutoffTimestamp, func(key int64, val any) bool {
userHistory.Iterate(0, uint64(cutoffTimestamp), func(key uint64, val any) bool {
userHistory.Remove(key)

return false // continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ func TestCleanTotalDelegationHistory(t *testing.T) {
name: "all entries after cutoff are preserved",
setupHistory: func() *staker.UintTree {
h := staker.NewUintTree()
h.Set(int64(2000), int64(500))
h.Set(int64(3000), int64(600))
h.Set(uint64(2000), int64(500))
h.Set(uint64(3000), int64(600))
return h
},
cutoffTimestamp: 1000,
Expand All @@ -45,10 +45,10 @@ func TestCleanTotalDelegationHistory(t *testing.T) {
name: "entries before cutoff are removed, last value preserved at cutoff",
setupHistory: func() *staker.UintTree {
h := staker.NewUintTree()
h.Set(int64(100), int64(100))
h.Set(int64(200), int64(200))
h.Set(int64(500), int64(500)) // most recent before cutoff
h.Set(int64(1500), int64(1500))
h.Set(uint64(100), int64(100))
h.Set(uint64(200), int64(200))
h.Set(uint64(500), int64(500)) // most recent before cutoff
h.Set(uint64(1500), int64(1500))
return h
},
cutoffTimestamp: 1000,
Expand All @@ -60,9 +60,9 @@ func TestCleanTotalDelegationHistory(t *testing.T) {
name: "all entries before cutoff leaves only preserved value",
setupHistory: func() *staker.UintTree {
h := staker.NewUintTree()
h.Set(int64(100), int64(100))
h.Set(int64(200), int64(200))
h.Set(int64(500), int64(999))
h.Set(uint64(100), int64(100))
h.Set(uint64(200), int64(200))
h.Set(uint64(500), int64(999))
return h
},
cutoffTimestamp: 1000,
Expand All @@ -74,9 +74,9 @@ func TestCleanTotalDelegationHistory(t *testing.T) {
name: "exact cutoff timestamp entry is preserved",
setupHistory: func() *staker.UintTree {
h := staker.NewUintTree()
h.Set(int64(500), int64(500))
h.Set(int64(1000), int64(1000)) // exact cutoff
h.Set(int64(1500), int64(1500))
h.Set(uint64(500), int64(500))
h.Set(uint64(1000), int64(1000)) // exact cutoff
h.Set(uint64(1500), int64(1500))
return h
},
cutoffTimestamp: 1000,
Expand All @@ -100,7 +100,7 @@ func TestCleanTotalDelegationHistory(t *testing.T) {

// Verify value at cutoff if there are entries
if tt.expectedEntries > 0 {
valueAtCutoff, exists := resultHistory.Get(tt.cutoffTimestamp)
valueAtCutoff, exists := resultHistory.Get(uint64(tt.cutoffTimestamp))
if tt.expectedValueAtCutoff > 0 {
uassert.True(t, exists)
uassert.Equal(t, tt.expectedValueAtCutoff, valueAtCutoff.(int64))
Expand Down Expand Up @@ -141,8 +141,8 @@ func TestCleanUserDelegationHistory(t *testing.T) {
setupHistory: func() *avl.Tree {
tree := avl.NewTree()
userHistory := staker.NewUintTree()
userHistory.Set(int64(2000), int64(500))
userHistory.Set(int64(3000), int64(600))
userHistory.Set(uint64(2000), int64(500))
userHistory.Set(uint64(3000), int64(600))
tree.Set(user1.String(), userHistory)
return tree
},
Expand All @@ -156,9 +156,9 @@ func TestCleanUserDelegationHistory(t *testing.T) {
setupHistory: func() *avl.Tree {
tree := avl.NewTree()
userHistory := staker.NewUintTree()
userHistory.Set(int64(100), int64(100))
userHistory.Set(int64(500), int64(500))
userHistory.Set(int64(1500), int64(1500))
userHistory.Set(uint64(100), int64(100))
userHistory.Set(uint64(500), int64(500))
userHistory.Set(uint64(1500), int64(1500))
tree.Set(user1.String(), userHistory)
return tree
},
Expand All @@ -173,12 +173,12 @@ func TestCleanUserDelegationHistory(t *testing.T) {
tree := avl.NewTree()

user1History := staker.NewUintTree()
user1History.Set(int64(500), int64(500))
user1History.Set(int64(1500), int64(1500))
user1History.Set(uint64(500), int64(500))
user1History.Set(uint64(1500), int64(1500))
tree.Set(user1.String(), user1History)

user2History := staker.NewUintTree()
user2History.Set(int64(2000), int64(2000))
user2History.Set(uint64(2000), int64(2000))
tree.Set(user2.String(), user2History)

return tree
Expand All @@ -193,8 +193,8 @@ func TestCleanUserDelegationHistory(t *testing.T) {
setupHistory: func() *avl.Tree {
tree := avl.NewTree()
userHistory := staker.NewUintTree()
userHistory.Set(int64(100), int64(100))
userHistory.Set(int64(500), int64(999))
userHistory.Set(uint64(100), int64(100))
userHistory.Set(uint64(500), int64(999))
tree.Set(user1.String(), userHistory)
return tree
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func TestStakerDelegationSnapshot_BoundaryConditions(t *testing.T) {
history := gs.store.GetTotalDelegationHistory()
history.Set(100, int64(1000))
history.Set(200, int64(2000))
history.Set(math.MaxInt64-1, int64(3000))
history.Set(uint64(math.MaxInt64-1), int64(3000))
err := gs.store.SetTotalDelegationHistory(history)
if err != nil {
panic(err)
Expand Down Expand Up @@ -150,7 +150,7 @@ func TestStakerDelegationSnapshot_BoundaryConditions(t *testing.T) {
userHistory := staker.NewUintTree()
userHistory.Set(100, int64(1000))
userHistory.Set(200, int64(2000))
userHistory.Set(math.MaxInt64-1, int64(3000))
userHistory.Set(uint64(math.MaxInt64-1), int64(3000))

history := avl.NewTree()
history.Set(aliceAddr, userHistory)
Expand Down Expand Up @@ -180,8 +180,8 @@ func TestStakerDelegationSnapshot_BoundaryConditions(t *testing.T) {
func(cur realm) {
history := gs.store.GetTotalDelegationHistory()
history.Set(100, int64(1000))
history.Set(math.MaxInt64-1000, int64(2000))
history.Set(math.MaxInt64-500, int64(3000))
history.Set(uint64(math.MaxInt64-1000), int64(2000))
history.Set(uint64(math.MaxInt64-500), int64(3000))
err := gs.store.SetTotalDelegationHistory(history)
if err != nil {
panic(err)
Expand All @@ -197,7 +197,7 @@ func TestStakerDelegationSnapshot_BoundaryConditions(t *testing.T) {
// then: should preserve continuity and clean old entries
history := gs.store.GetTotalDelegationHistory()
// Should have at least the cutoff entry
val, exists := history.Get(cutoff)
val, exists := history.Get(uint64(cutoff))
uassert.True(t, exists || history.Size() > 0)
_ = val
})
Expand All @@ -213,8 +213,8 @@ func TestStakerDelegationSnapshot_BoundaryConditions(t *testing.T) {
func(cur realm) {
userHistory := staker.NewUintTree()
userHistory.Set(100, int64(1000))
userHistory.Set(math.MaxInt64-1000, int64(2000))
userHistory.Set(math.MaxInt64-500, int64(3000))
userHistory.Set(uint64(math.MaxInt64-1000), int64(2000))
userHistory.Set(uint64(math.MaxInt64-500), int64(3000))

history := avl.NewTree()
history.Set(aliceAddr, userHistory)
Expand Down
Loading
Loading