Skip to content
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
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
143 changes: 143 additions & 0 deletions internal/interpreter/funds_queue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
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.Sign() > 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)
if offset == 0 {
s.senders = s.senders[1:]
} else {
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])
if offset == 0 {
s.senders = s.senders[1:]
} else {
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