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

wallet: fix wallet Redistribute when outputs is larger than batchsize #170

Merged
merged 2 commits into from
Feb 10, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

# Fixes an issue where redistribution of wallet outputs that require more than a single transaction would produce an invalid transaction set
18 changes: 14 additions & 4 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ func (sw *SingleAddressWallet) selectRedistributeUTXOs(bh uint64, outputs int, a
// Redistribute returns a transaction that redistributes money in the wallet by
// selecting a minimal set of inputs to cover the creation of the requested
// outputs. It also returns a list of output IDs that need to be signed.
func (sw *SingleAddressWallet) Redistribute(outputs int, amount, feePerByte types.Currency) (txns []types.Transaction, toSign []types.Hash256, err error) {
func (sw *SingleAddressWallet) Redistribute(outputs int, amount, feePerByte types.Currency) (txns []types.Transaction, toSign [][]types.Hash256, err error) {
state := sw.cm.TipState()

elements, err := sw.store.UnspentSiacoinElements()
Expand All @@ -653,8 +653,10 @@ func (sw *SingleAddressWallet) Redistribute(outputs int, amount, feePerByte type
// in case of an error we need to free all inputs
defer func() {
if err != nil {
for _, id := range toSign {
delete(sw.locked, types.SiacoinOutputID(id))
for _, ids := range toSign {
for _, id := range ids {
delete(sw.locked, types.SiacoinOutputID(id))
}
}
}
}()
Expand Down Expand Up @@ -690,6 +692,9 @@ func (sw *SingleAddressWallet) Redistribute(outputs int, amount, feePerByte type
}
}

// remove used inputs from utxos
utxos = utxos[len(inputs):]

// not enough outputs found
fee := feePerInput.Mul64(uint64(len(inputs))).Add(outputFees)
if sumOut := SumOutputs(inputs); sumOut.Cmp(want.Add(fee)) < 0 {
Expand All @@ -711,15 +716,17 @@ func (sw *SingleAddressWallet) Redistribute(outputs int, amount, feePerByte type
}

// add the inputs
toSignTxn := make([]types.Hash256, 0, len(inputs))
for _, sce := range inputs {
toSignTxn = append(toSignTxn, types.Hash256(sce.ID))
txn.SiacoinInputs = append(txn.SiacoinInputs, types.SiacoinInput{
ParentID: types.SiacoinOutputID(sce.ID),
UnlockConditions: types.StandardUnlockConditions(sw.priv.PublicKey()),
})
toSign = append(toSign, types.Hash256(sce.ID))
sw.locked[sce.ID] = time.Now().Add(sw.cfg.ReservationDuration)
}
txns = append(txns, txn)
toSign = append(toSign, toSignTxn)
}

return
Expand Down Expand Up @@ -786,6 +793,9 @@ func (sw *SingleAddressWallet) RedistributeV2(outputs int, amount, feePerByte ty
}
}

// remove used inputs from utxos
utxos = utxos[len(inputs):]

// not enough outputs found
fee := feePerInput.Mul64(uint64(len(inputs))).Add(outputFees)
if sumOut := SumOutputs(inputs); sumOut.Cmp(want.Add(fee)) < 0 {
Expand Down
24 changes: 23 additions & 1 deletion wallet/wallet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ func TestWalletRedistribute(t *testing.T) {
}

for i := 0; i < len(txns); i++ {
w.SignTransaction(&txns[i], toSign, types.CoveredFields{WholeTransaction: true})
w.SignTransaction(&txns[i], toSign[i], types.CoveredFields{WholeTransaction: true})
}
if _, err := cm.AddPoolTransactions(txns); err != nil {
return fmt.Errorf("failed to add transactions to pool: %w", err)
Expand Down Expand Up @@ -557,6 +557,17 @@ func TestWalletRedistribute(t *testing.T) {
} else if len(toSign) != 0 {
t.Fatalf("expected no ids, got %v", len(toSign))
}

// redistribute the wallet into more outputs than the batch size to make
// sure the resulting txn set contains more than 1 txn
outputs, err := w.SpendableOutputs()
if err != nil {
t.Fatal(err)
} else if len(outputs) >= 11 {
t.Fatalf("expected at least 11 outputs, got %v", len(outputs))
} else if err := redistribute(types.Siacoins(1e3), 11); err != nil {
t.Fatal(err)
}
}

func TestWalletRedistributeV2(t *testing.T) {
Expand Down Expand Up @@ -652,6 +663,17 @@ func TestWalletRedistributeV2(t *testing.T) {
} else if len(toSign) != 0 {
t.Fatalf("expected no ids, got %v", len(toSign))
}

// redistribute the wallet into more outputs than the batch size to make
// sure the resulting txn set contains more than 1 txn
outputs, err := w.SpendableOutputs()
if err != nil {
t.Fatal(err)
} else if len(outputs) >= 11 {
t.Fatalf("expected at least 11 outputs, got %v", len(outputs))
} else if err := redistribute(types.Siacoins(1e3), 11); err != nil {
t.Fatal(err)
}
}

func TestReorg(t *testing.T) {
Expand Down
Loading