Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 3 additions & 3 deletions internal/interpreter/batch_balances_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (st *programState) findBalancesQueriesInStatement(statement parser.Statemen
if err != nil {
return err
}
st.CurrentAsset = *asset
st.fundsQueue.asset = *asset

// traverse source
return st.findBalancesQueries(statement.Source)
Expand Down Expand Up @@ -95,7 +95,7 @@ func (st *programState) findBalancesQueries(source parser.Source) InterpreterErr
return err
}

st.batchQuery(*account, st.CurrentAsset, color)
st.batchQuery(*account, st.fundsQueue.asset, color)
return nil

case *parser.SourceOverdraft:
Expand All @@ -113,7 +113,7 @@ func (st *programState) findBalancesQueries(source parser.Source) InterpreterErr
return err
}

st.batchQuery(*account, st.CurrentAsset, color)
st.batchQuery(*account, st.fundsQueue.asset, color)
return nil

case *parser.SourceInorder:
Expand Down
135 changes: 135 additions & 0 deletions internal/interpreter/funds_queue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package interpreter

import (
"fmt"
"math/big"
"slices"
)

type Sender struct {
Name string
Amount *big.Int
Color string
}

type fundsQueue struct {
asset string
senders []Sender
}

// Create a fundsQueue from a slice of senders.
func newFundsQueue(senders []Sender) fundsQueue {
queue := fundsQueue{
senders: []Sender{},
}
queue.Push(senders...)
return queue
}

// Push senders to this fundsQueue
func (s *fundsQueue) Push(senders ...Sender) {
for _, sender := range senders {
s.PushOne(sender)
}
}

// Push a single sender to this queue
func (s *fundsQueue) PushOne(sender Sender) {
if sender.Amount.Cmp(big.NewInt(0)) == 0 {
return
}
if len(s.senders) == 0 {
s.senders = []Sender{sender}
return
}
last := s.senders[len(s.senders)-1]
if last.Name == sender.Name && last.Color == sender.Color {
last.Amount.Add(last.Amount, sender.Amount)
} else {
s.senders = append(s.senders, sender)
}
}

// Pull everything from this queue
func (s *fundsQueue) PullAll() []Sender {
senders := s.senders
s.senders = []Sender{}
return senders
}

// Pull at most maxAmount from this queue, with any color
func (s *fundsQueue) PullAnything(maxAmount *big.Int) []Sender {
return s.Pull(maxAmount, nil)
}

func (s *fundsQueue) PullColored(maxAmount *big.Int, color string) []Sender {
return s.Pull(maxAmount, &color)
}
func (s *fundsQueue) PullUncolored(maxAmount *big.Int) []Sender {
return s.PullColored(maxAmount, "")
}

// Pull at most maxAmount from this queue, with the given color
func (s *fundsQueue) Pull(maxAmount *big.Int, color *string) []Sender {
// clone so that we can manipulate this arg
maxAmount = new(big.Int).Set(maxAmount)

// TODO preallocate for perfs
out := newFundsQueue([]Sender{})
offset := 0

for maxAmount.Cmp(big.NewInt(0)) != 0 && len(s.senders) > offset {

frontSender := s.senders[offset]

if color != nil && frontSender.Color != *color {
offset += 1
continue
}

switch frontSender.Amount.Cmp(maxAmount) {
case -1: // not enough
maxAmount.Sub(maxAmount, frontSender.Amount)
out.Push(frontSender)
s.senders = slices.Delete(s.senders, offset, offset+1)
case 1: // more than enough
out.Push(Sender{
Name: frontSender.Name,
Amount: maxAmount,
Color: frontSender.Color,
})
s.senders[offset].Amount.Sub(s.senders[offset].Amount, maxAmount)
return out.senders
case 0: // exactly enough
out.Push(s.senders[offset])
s.senders = slices.Delete(s.senders, offset, offset+1)
return out.senders
}
}

return out.senders
}

// Clone the queue so that you can safely mutate one without mutating the other
func (s fundsQueue) Clone() fundsQueue {
return fundsQueue{
senders: slices.Clone(s.senders),
asset: s.asset,
}
Comment on lines +122 to +126
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Deep-clone sender amounts in Clone()

Clone() only copies the slice, so every cloned Sender.Amount still points to the same *big.Int. When cloneState() (interpreter.go Line 604) restores fundsQueue, subsequent mutations done after the clone leak back into the “restored” queue, breaking backtracking (e.g. oneof sources will see already-depleted amounts). Please deep-copy each amount.

 func (s fundsQueue) Clone() fundsQueue {
-	return fundsQueue{
-		senders: slices.Clone(s.senders),
-		asset:   s.asset,
-	}
+	cloned := make([]Sender, len(s.senders))
+	for i, sender := range s.senders {
+		cloned[i] = Sender{
+			Name:   sender.Name,
+			Color:  sender.Color,
+			Amount: new(big.Int).Set(sender.Amount),
+		}
+	}
+	return fundsQueue{
+		senders: cloned,
+		asset:   s.asset,
+	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s fundsQueue) Clone() fundsQueue {
return fundsQueue{
senders: slices.Clone(s.senders),
asset: s.asset,
}
func (s fundsQueue) Clone() fundsQueue {
cloned := make([]Sender, len(s.senders))
for i, sender := range s.senders {
cloned[i] = Sender{
Name: sender.Name,
Color: sender.Color,
Amount: new(big.Int).Set(sender.Amount),
}
}
return fundsQueue{
senders: cloned,
asset: s.asset,
}
}
🤖 Prompt for AI Agents
In internal/interpreter/funds_queue.go around lines 114 to 118, Clone()
currently shallow-copies the senders slice so each Sender.Amount still points to
the same *big.Int; change Clone() to deep-copy each Sender and its Amount by
creating a new slice of senders, copying each Sender struct and, for each
non-nil Amount, allocating a new big.Int and calling Set(oldAmount) (or using
new(big.Int).Set) so the cloned fundsQueue has independent amount values and
subsequent mutations do not affect the original.

}

func (s fundsQueue) String() string {
out := ">"
for i, sender := range s.senders {
if sender.Color == "" {
out += fmt.Sprintf("%v from %v", sender.Amount, sender.Name)
} else {
out += fmt.Sprintf("%v from %v\\%v", sender.Amount, sender.Name, sender.Color)
}
if i != len(s.senders)-1 {
out += ", "
}
}
out += ">"
return out
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,146 +8,147 @@ import (
)

func TestEnoughBalance(t *testing.T) {
stack := newFundsStack([]Sender{
queue := newFundsQueue([]Sender{
{Name: "s1", Amount: big.NewInt(100)},
})

out := stack.PullAnything(big.NewInt(2))
out := queue.PullAnything(big.NewInt(2))
require.Equal(t, []Sender{
{Name: "s1", Amount: big.NewInt(2)},
}, out)

}

func TestPush(t *testing.T) {
stack := newFundsStack(nil)
stack.Push(Sender{Name: "acc", Amount: big.NewInt(100)})
queue := newFundsQueue(nil)
queue.Push(Sender{Name: "acc", Amount: big.NewInt(100)})

out := stack.PullUncolored(big.NewInt(20))
out := queue.PullUncolored(big.NewInt(20))
require.Equal(t, []Sender{
{Name: "acc", Amount: big.NewInt(20)},
}, out)

}

func TestSimple(t *testing.T) {
stack := newFundsStack([]Sender{
queue := newFundsQueue([]Sender{
{Name: "s1", Amount: big.NewInt(2)},
{Name: "s2", Amount: big.NewInt(10)},
})

out := stack.PullAnything(big.NewInt(5))
out := queue.PullAnything(big.NewInt(5))
require.Equal(t, []Sender{
{Name: "s1", Amount: big.NewInt(2)},
{Name: "s2", Amount: big.NewInt(3)},
}, out)

out = stack.PullAnything(big.NewInt(7))
out = queue.PullAnything(big.NewInt(7))
require.Equal(t, []Sender{
{Name: "s2", Amount: big.NewInt(7)},
}, out)
}

func TestPullZero(t *testing.T) {
stack := newFundsStack([]Sender{
queue := newFundsQueue([]Sender{
{Name: "s1", Amount: big.NewInt(2)},
{Name: "s2", Amount: big.NewInt(10)},
})

out := stack.PullAnything(big.NewInt(0))
require.Equal(t, []Sender(nil), out)
out := queue.PullAnything(big.NewInt(0))
require.Equal(t, []Sender{}, out)
}

func TestCompactFunds(t *testing.T) {
stack := newFundsStack([]Sender{
queue := newFundsQueue([]Sender{
{Name: "s1", Amount: big.NewInt(2)},
{Name: "s1", Amount: big.NewInt(10)},
})

out := stack.PullAnything(big.NewInt(5))
out := queue.PullAnything(big.NewInt(5))
require.Equal(t, []Sender{
{Name: "s1", Amount: big.NewInt(5)},
}, out)
}

func TestCompactFunds3Times(t *testing.T) {
stack := newFundsStack([]Sender{
queue := newFundsQueue([]Sender{
{Name: "s1", Amount: big.NewInt(2)},
{Name: "s1", Amount: big.NewInt(3)},
{Name: "s1", Amount: big.NewInt(1)},
})

out := stack.PullAnything(big.NewInt(6))
out := queue.PullAnything(big.NewInt(6))
require.Equal(t, []Sender{
{Name: "s1", Amount: big.NewInt(6)},
}, out)
}

func TestCompactFundsWithEmptySender(t *testing.T) {
stack := newFundsStack([]Sender{
queue := newFundsQueue([]Sender{
{Name: "s1", Amount: big.NewInt(2)},
{Name: "s2", Amount: big.NewInt(0)},
{Name: "s1", Amount: big.NewInt(10)},
})

out := stack.PullAnything(big.NewInt(5))
out := queue.PullAnything(big.NewInt(5))

require.Equal(t, []Sender{
{Name: "s1", Amount: big.NewInt(5)},
}, out)
}

func TestMissingFunds(t *testing.T) {
stack := newFundsStack([]Sender{
queue := newFundsQueue([]Sender{
{Name: "s1", Amount: big.NewInt(2)},
})

out := stack.PullAnything(big.NewInt(300))
out := queue.PullAnything(big.NewInt(300))
require.Equal(t, []Sender{
{Name: "s1", Amount: big.NewInt(2)},
}, out)
}

func TestNoZeroLeftovers(t *testing.T) {
stack := newFundsStack([]Sender{
queue := newFundsQueue([]Sender{
{Name: "s1", Amount: big.NewInt(10)},
{Name: "s2", Amount: big.NewInt(15)},
})

stack.PullAnything(big.NewInt(10))
queue.PullAnything(big.NewInt(10))

out := stack.PullAnything(big.NewInt(15))
out := queue.PullAnything(big.NewInt(15))
require.Equal(t, []Sender{
{Name: "s2", Amount: big.NewInt(15)},
}, out)
}

func TestReconcileColoredManyDestPerSender(t *testing.T) {
stack := newFundsStack([]Sender{
queue := newFundsQueue([]Sender{
{"src", big.NewInt(10), "X"},
})

out := stack.PullColored(big.NewInt(5), "X")
out := queue.PullColored(big.NewInt(5), "X")
require.Equal(t, []Sender{
{Name: "src", Amount: big.NewInt(5), Color: "X"},
}, out)

out = stack.PullColored(big.NewInt(5), "X")
out = queue.PullColored(big.NewInt(5), "X")
require.Equal(t, []Sender{
{Name: "src", Amount: big.NewInt(5), Color: "X"},
}, out)

}

func TestPullColored(t *testing.T) {
stack := newFundsStack([]Sender{
queue := newFundsQueue([]Sender{
{Name: "s1", Amount: big.NewInt(5)},
{Name: "s2", Amount: big.NewInt(1), Color: "red"},
{Name: "s3", Amount: big.NewInt(10)},
{Name: "s4", Amount: big.NewInt(2), Color: "red"},
{Name: "s5", Amount: big.NewInt(5)},
})

out := stack.PullColored(big.NewInt(2), "red")
out := queue.PullColored(big.NewInt(2), "red")
require.Equal(t, []Sender{
{Name: "s2", Amount: big.NewInt(1), Color: "red"},
{Name: "s4", Amount: big.NewInt(1), Color: "red"},
Expand All @@ -158,24 +159,24 @@ func TestPullColored(t *testing.T) {
{Name: "s3", Amount: big.NewInt(10)},
{Name: "s4", Amount: big.NewInt(1), Color: "red"},
{Name: "s5", Amount: big.NewInt(5)},
}, stack.PullAll())
}, queue.PullAll())
}

func TestPullColoredComplex(t *testing.T) {
stack := newFundsStack([]Sender{
queue := newFundsQueue([]Sender{
{"s1", big.NewInt(1), "c1"},
{"s2", big.NewInt(1), "c2"},
})

out := stack.PullColored(big.NewInt(1), "c2")
out := queue.PullColored(big.NewInt(1), "c2")
require.Equal(t, []Sender{
{Name: "s2", Amount: big.NewInt(1), Color: "c2"},
}, out)
}

func TestClone(t *testing.T) {

fs := newFundsStack([]Sender{
fs := newFundsQueue([]Sender{
{"s1", big.NewInt(10), ""},
})

Expand All @@ -192,19 +193,19 @@ func TestClone(t *testing.T) {
func TestCompactFundsAndPush(t *testing.T) {
noCol := ""

stack := newFundsStack([]Sender{
queue := newFundsQueue([]Sender{
{Name: "s1", Amount: big.NewInt(2)},
{Name: "s1", Amount: big.NewInt(10)},
})

stack.Pull(big.NewInt(1), &noCol)
queue.Pull(big.NewInt(1), &noCol)

stack.Push(Sender{
queue.Push(Sender{
Name: "pushed",
Amount: big.NewInt(42),
})

out := stack.PullAll()
out := queue.PullAll()
require.Equal(t, []Sender{
{Name: "s1", Amount: big.NewInt(11)},
{Name: "pushed", Amount: big.NewInt(42)},
Expand Down
Loading
Loading