Skip to content

Commit

Permalink
Merge pull request #240 from SiaFoundation/nate/wallet-fix-funding
Browse files Browse the repository at this point in the history
Fix renewal funding
  • Loading branch information
n8maninger authored Dec 15, 2023
2 parents 0a28144 + 99cb781 commit d29812a
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 11 deletions.
4 changes: 2 additions & 2 deletions internal/test/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ func (w *Wallet) SendSiacoins(outputs []types.SiacoinOutput) (txn types.Transact
}
defer release()
if err := w.SignTransaction(w.ChainManager().TipState(), &txn, toSign, types.CoveredFields{WholeTransaction: true}); err != nil {
return types.Transaction{}, fmt.Errorf("failed to sign transaction: %w", err)
return txn, fmt.Errorf("failed to sign transaction: %w", err)
} else if err := w.tp.AcceptTransactionSet([]types.Transaction{txn}); err != nil {
return types.Transaction{}, fmt.Errorf("failed to accept transaction set: %w", err)
return txn, fmt.Errorf("failed to accept transaction set: %w", err)
}
return txn, nil
}
Expand Down
66 changes: 59 additions & 7 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
"sort"
"sync"
"sync/atomic"
"time"
Expand All @@ -18,6 +19,18 @@ import (
"go.uber.org/zap"
)

const (
// transactionDefragThreshold is the number of utxos at which the wallet
// will attempt to defrag itself by including small utxos in transactions.
transactionDefragThreshold = 30
// maxInputsForDefrag is the maximum number of inputs a transaction can
// have before the wallet will stop adding inputs
maxInputsForDefrag = 30
// maxDefragUTXOs is the maximum number of utxos that will be added to a
// transaction when defragging
maxDefragUTXOs = 10
)

// transaction sources indicate the source of a transaction. Transactions can
// either be created by sending Siacoins between unlock hashes or they can be
// created by consensus (e.g. a miner payout, a siafund claim, or a contract).
Expand Down Expand Up @@ -267,29 +280,68 @@ func (sw *SingleAddressWallet) FundTransaction(txn *types.Transaction, amount ty
if err != nil {
return nil, nil, err
}
var inputSum types.Currency
var fundingElements []SiacoinElement

// remove locked and spent outputs
usableUTXOs := utxos[:0]
for _, sce := range utxos {
if sw.locked[sce.ID] || sw.tpoolSpent[sce.ID] || sw.consensusLocked[sce.ID] {
continue
}
fundingElements = append(fundingElements, sce)
inputSum = inputSum.Add(sce.Value)
usableUTXOs = append(usableUTXOs, sce)
}

// sort by value, descending
sort.Slice(usableUTXOs, func(i, j int) bool {
return usableUTXOs[i].Value.Cmp(usableUTXOs[j].Value) > 0
})

// fund the transaction using the largest utxos first
var selected []SiacoinElement
var inputSum types.Currency
for i, sce := range usableUTXOs {
if inputSum.Cmp(amount) >= 0 {
usableUTXOs = usableUTXOs[i:]
break
}
selected = append(selected, sce)
inputSum = inputSum.Add(sce.Value)
}

// if the transaction can't be funded, return an error
if inputSum.Cmp(amount) < 0 {
return nil, nil, ErrNotEnoughFunds
} else if inputSum.Cmp(amount) > 0 {
}

// check if remaining utxos should be defragged
txnInputs := len(txn.SiacoinInputs) + len(selected)
if len(usableUTXOs) > transactionDefragThreshold && txnInputs < maxInputsForDefrag {
// add the smallest utxos to the transaction
defraggable := usableUTXOs
if len(defraggable) > maxDefragUTXOs {
defraggable = defraggable[len(defraggable)-maxDefragUTXOs:]
}
for i := len(defraggable) - 1; i >= 0; i-- {
if txnInputs >= maxInputsForDefrag {
break
}

sce := defraggable[i]
selected = append(selected, sce)
inputSum = inputSum.Add(sce.Value)
txnInputs++
}
}

// add a change output if necessary
if inputSum.Cmp(amount) > 0 {
txn.SiacoinOutputs = append(txn.SiacoinOutputs, types.SiacoinOutput{
Value: inputSum.Sub(amount),
Address: sw.addr,
})
}

toSign := make([]types.Hash256, len(fundingElements))
for i, sce := range fundingElements {
toSign := make([]types.Hash256, len(selected))
for i, sce := range selected {
txn.SiacoinInputs = append(txn.SiacoinInputs, types.SiacoinInput{
ParentID: types.SiacoinOutputID(sce.ID),
UnlockConditions: types.StandardUnlockConditions(sw.priv.PublicKey()),
Expand Down
84 changes: 82 additions & 2 deletions wallet/wallet_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package wallet_test

import (
"encoding/json"
"sort"
"testing"
"time"

Expand Down Expand Up @@ -90,12 +92,14 @@ func TestWallet(t *testing.T) {
Address: w.Address(),
}
}
if _, err = w.SendSiacoins(splitOutputs); err != nil {
if txn, err := w.SendSiacoins(splitOutputs); err != nil {
buf, _ := json.MarshalIndent(txn, "", " ")
t.Log(string(buf))
t.Fatal(err)
}

time.Sleep(250 * time.Millisecond) // sleep for tpool sync
// check that the wallet's spendable balance and unconfiremed balance are
// check that the wallet's spendable balance and unconfirmed balance are
// correct
spendable, balance, unconfirmed, err := w.Balance()
if err != nil {
Expand Down Expand Up @@ -325,3 +329,79 @@ func TestWalletReset(t *testing.T) {
t.Fatal("expected zero balance")
}
}

func TestWalletUTXOSelection(t *testing.T) {
log := zaptest.NewLogger(t)
w, err := test.NewWallet(types.GeneratePrivateKey(), t.TempDir(), log.Named("wallet"))
if err != nil {
t.Fatal(err)
}
defer w.Close()

_, balance, _, err := w.Balance()
if err != nil {
t.Fatal(err)
} else if !balance.Equals(types.ZeroCurrency) {
t.Fatalf("expected zero balance, got %v", balance)
}

// mine until the wallet has 100 mature outputs
if err := w.MineBlocks(w.Address(), 100+int(stypes.MaturityDelay)); err != nil {
t.Fatal(err)
}

time.Sleep(time.Second) // sleep for consensus sync

// check that the expected utxos were used
utxos, err := w.Store().UnspentSiacoinElements()
if err != nil {
t.Fatal(err)
} else if len(utxos) != 100 {
t.Fatalf("expected 100 utxos, got %v", len(utxos))
}
sort.Slice(utxos, func(i, j int) bool {
return utxos[i].Value.Cmp(utxos[j].Value) > 0
})

// send a transaction to the burn address
sendAmount := types.Siacoins(10)
minerFee := types.Siacoins(1)
txn := types.Transaction{
MinerFees: []types.Currency{minerFee},
SiacoinOutputs: []types.SiacoinOutput{
{Address: types.VoidAddress, Value: sendAmount},
},
}

fundAmount := sendAmount.Add(minerFee)
toSign, release, err := w.FundTransaction(&txn, fundAmount)
if err != nil {
t.Fatal(err)
}
defer release()

if len(txn.SiacoinInputs) != 11 {
t.Fatalf("expected 10 additional defrag inputs, got %v", len(toSign)-1)
} else if len(txn.SiacoinOutputs) != 2 {
t.Fatalf("expected a change output, got %v", len(txn.SiacoinOutputs))
}

// check that the expected UTXOs were added
spent := []wallet.SiacoinElement{utxos[0]}
rem := utxos[90:]
for i := len(rem) - 1; i >= 0; i-- {
spent = append(spent, rem[i])
}

for i := range txn.SiacoinInputs {
if txn.SiacoinInputs[i].ParentID != spent[i].ID {
t.Fatalf("expected input %v to spend %v, got %v", i, spent[i].ID, txn.SiacoinInputs[i].ParentID)
}
}

if err := w.SignTransaction(w.TipState(), &txn, toSign, types.CoveredFields{WholeTransaction: true}); err != nil {
t.Fatal(err)
} else if err := w.TPool().AcceptTransactionSet([]types.Transaction{txn}); err != nil {
t.Fatal(err)
}
}

0 comments on commit d29812a

Please sign in to comment.