Skip to content

Commit

Permalink
wallet: add useUnconfirmed to FundTransaction
Browse files Browse the repository at this point in the history
  • Loading branch information
n8maninger committed Jan 13, 2024
1 parent cee8d7b commit d0138bd
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 39 deletions.
13 changes: 7 additions & 6 deletions testutil/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ func Network() (*consensus.Network, types.Block) {
}

// MineBlock mines a block with the given transactions.
func MineBlock(cs consensus.State, transactions []types.Transaction, minerAddress types.Address) types.Block {
func MineBlock(cm *chain.Manager, minerAddress types.Address) types.Block {
state := cm.TipState()
b := types.Block{
ParentID: cs.Index.ID,
ParentID: state.Index.ID,
Timestamp: types.CurrentTimestamp(),
Transactions: transactions,
MinerPayouts: []types.SiacoinOutput{{Address: minerAddress, Value: cs.BlockReward()}},
Transactions: cm.PoolTransactions(),
MinerPayouts: []types.SiacoinOutput{{Address: minerAddress, Value: state.BlockReward()}},
}
for b.ID().CmpWork(cs.ChildTarget) < 0 {
b.Nonce += cs.NonceFactor()
for b.ID().CmpWork(state.ChildTarget) < 0 {
b.Nonce += state.NonceFactor()
}
return b
}
65 changes: 50 additions & 15 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type (

// A ChainManager manages the current state of the blockchain.
ChainManager interface {
TipState() consensus.State
BestIndex(height uint64) (types.ChainIndex, bool)

PoolTransactions() []types.Transaction
Expand Down Expand Up @@ -152,6 +153,10 @@ func (sw *SingleAddressWallet) Balance() (spendable, confirmed, unconfirmed type
delete(tpoolUtxos, types.Hash256(sci.ParentID))
}
for i, sco := range txn.SiacoinOutputs {
if sco.Address != sw.addr {
continue
}

tpoolUtxos[types.Hash256(txn.SiacoinOutputID(i))] = types.SiacoinElement{
StateElement: types.StateElement{
ID: types.Hash256(types.SiacoinOutputID(txn.SiacoinOutputID(i))),
Expand Down Expand Up @@ -192,7 +197,7 @@ func (sw *SingleAddressWallet) TransactionCount() (uint64, error) {
// transaction. If necessary, a change output will also be added. The inputs
// will not be available to future calls to FundTransaction unless ReleaseInputs
// is called.
func (sw *SingleAddressWallet) FundTransaction(txn *types.Transaction, amount types.Currency) ([]types.Hash256, func(), error) {
func (sw *SingleAddressWallet) FundTransaction(txn *types.Transaction, amount types.Currency, useUnconfirmed bool) ([]types.Hash256, func(), error) {
if amount.IsZero() {
return nil, func() {}, nil
}
Expand Down Expand Up @@ -223,41 +228,70 @@ func (sw *SingleAddressWallet) FundTransaction(txn *types.Transaction, amount ty
}

// remove locked and spent outputs
usableUTXOs := utxos[:0]
filtered := utxos[:0]
for _, sce := range utxos {
if time.Now().Before(sw.locked[sce.ID]) || tpoolSpent[sce.ID] {
continue
}
usableUTXOs = append(usableUTXOs, sce)
filtered = append(filtered, sce)
}
utxos = filtered

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

var unconfirmedUTXOs []types.SiacoinElement
if useUnconfirmed {
for _, sce := range tpoolUtxos {
if sce.SiacoinOutput.Address != sw.addr || time.Now().Before(sw.locked[sce.ID]) {
continue
}
unconfirmedUTXOs = append(unconfirmedUTXOs, sce)
}

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

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

// if the transaction can't be funded, return an error
if inputSum.Cmp(amount) < 0 {
if inputSum.Cmp(amount) < 0 && useUnconfirmed {
// try adding unconfirmed utxos.
for _, sce := range unconfirmedUTXOs {
if inputSum.Cmp(amount) >= 0 {
break
}
selected = append(selected, sce)
inputSum = inputSum.Add(sce.SiacoinOutput.Value)
}

if inputSum.Cmp(amount) < 0 {
// still not enough funds
return nil, nil, ErrNotEnoughFunds
}
} else if inputSum.Cmp(amount) < 0 {
return nil, nil, ErrNotEnoughFunds
}

// check if remaining utxos should be defragged
txnInputs := len(txn.SiacoinInputs) + len(selected)
if len(usableUTXOs) > sw.cfg.DefragThreshold && txnInputs < sw.cfg.MaxInputsForDefrag {
if len(utxos) > sw.cfg.DefragThreshold && txnInputs < sw.cfg.MaxInputsForDefrag {
// add the smallest utxos to the transaction
defraggable := usableUTXOs
defraggable := utxos
if len(defraggable) > sw.cfg.MaxDefragUTXOs {
defraggable = defraggable[len(defraggable)-sw.cfg.MaxDefragUTXOs:]
}
Expand Down Expand Up @@ -302,13 +336,15 @@ func (sw *SingleAddressWallet) FundTransaction(txn *types.Transaction, amount ty
}

// SignTransaction adds a signature to each of the specified inputs.
func (sw *SingleAddressWallet) SignTransaction(cs consensus.State, txn *types.Transaction, toSign []types.Hash256, cf types.CoveredFields) error {
func (sw *SingleAddressWallet) SignTransaction(txn *types.Transaction, toSign []types.Hash256, cf types.CoveredFields) {
state := sw.cm.TipState()

for _, id := range toSign {
var h types.Hash256
if cf.WholeTransaction {
h = cs.WholeSigHash(*txn, id, 0, 0, cf.Signatures)
h = state.WholeSigHash(*txn, id, 0, 0, cf.Signatures)
} else {
h = cs.PartialSigHash(*txn, cf)
h = state.PartialSigHash(*txn, cf)
}
sig := sw.priv.SignHash(h)
txn.Signatures = append(txn.Signatures, types.TransactionSignature{
Expand All @@ -318,7 +354,6 @@ func (sw *SingleAddressWallet) SignTransaction(cs consensus.State, txn *types.Tr
Signature: sig[:],
})
}
return nil
}

// Tip returns the block height the wallet has scanned to.
Expand Down
162 changes: 144 additions & 18 deletions wallet/wallet_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package wallet_test

import (
"errors"
"testing"

"go.sia.tech/core/chain"
Expand Down Expand Up @@ -47,22 +48,20 @@ func TestWallet(t *testing.T) {
}

initialReward := cm.TipState().BlockReward()
tip := cm.TipState()
// mine a block to fund the wallet
b := testutil.MineBlock(tip, nil, w.Address())
b := testutil.MineBlock(cm, w.Address())
if err := cm.AddBlocks([]types.Block{b}); err != nil {
t.Fatal(err)
}
tip = cm.TipState()

// mine until the payout matures
tip := cm.TipState()
target := tip.MaturityHeight() + 1
for i := tip.Index.Height; i < target; i++ {
b := testutil.MineBlock(tip, nil, types.VoidAddress)
b := testutil.MineBlock(cm, types.VoidAddress)
if err := cm.AddBlocks([]types.Block{b}); err != nil {
t.Fatal(err)
}
tip = cm.TipState()
}

// check that one payout has matured
Expand Down Expand Up @@ -107,15 +106,12 @@ func TestWallet(t *testing.T) {
}

// fund and sign the transaction
toSign, release, err := w.FundTransaction(&txn, initialReward)
toSign, release, err := w.FundTransaction(&txn, initialReward, false)
if err != nil {
t.Fatal(err)
}
defer release()

if err := w.SignTransaction(tip, &txn, toSign, types.CoveredFields{WholeTransaction: true}); err != nil {
t.Fatal(err)
}
w.SignTransaction(&txn, toSign, types.CoveredFields{WholeTransaction: true})

// check that wallet now has no spendable balance
spendable, confirmed, unconfirmed, err = w.Balance()
Expand Down Expand Up @@ -172,8 +168,7 @@ func TestWallet(t *testing.T) {
}

// mine a block to confirm the transaction
tip = cm.TipState()
b = testutil.MineBlock(tip, []types.Transaction{txn}, types.VoidAddress)
b = testutil.MineBlock(cm, types.VoidAddress)
if err := cm.AddBlocks([]types.Block{b}); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -216,19 +211,20 @@ func TestWallet(t *testing.T) {
{Address: types.VoidAddress, Value: sendAmount},
}

toSign, release, err := w.FundTransaction(&sent[i], sendAmount)
toSign, release, err := w.FundTransaction(&sent[i], sendAmount, false)
if err != nil {
t.Fatal(err)
}
defer release()
w.SignTransaction(&sent[i], toSign, types.CoveredFields{WholeTransaction: true})
}

if err := w.SignTransaction(tip, &sent[i], toSign, types.CoveredFields{WholeTransaction: true}); err != nil {
t.Fatal(err)
}
// add the transactions to the pool
if _, err := cm.AddPoolTransactions(sent); err != nil {
t.Fatal(err)
}

tip = cm.TipState()
b = testutil.MineBlock(tip, sent, types.VoidAddress)
b = testutil.MineBlock(cm, types.VoidAddress)
if err := cm.AddBlocks([]types.Block{b}); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -269,3 +265,133 @@ func TestWallet(t *testing.T) {
}
}
}

func TestWalletUnconfirmed(t *testing.T) {
log := zaptest.NewLogger(t)

network, genesis := testutil.Network()

cs, tipState, err := chain.NewDBStore(chain.NewMemDB(), network, genesis)
if err != nil {
t.Fatal(err)
}

cm := chain.NewManager(cs, tipState)

pk := types.GeneratePrivateKey()
ws := testutil.NewEphemeralWalletStore(pk)

if err := cm.AddSubscriber(ws, types.ChainIndex{}); err != nil {
t.Fatal(err)
}

w, err := wallet.NewSingleAddressWallet(pk, cm, ws, wallet.WithLogger(log.Named("wallet")))
if err != nil {
t.Fatal(err)
}
defer w.Close()

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

initialReward := cm.TipState().BlockReward()
// mine a block to fund the wallet
b := testutil.MineBlock(cm, w.Address())
if err := cm.AddBlocks([]types.Block{b}); err != nil {
t.Fatal(err)
}

// mine until the payout matures
tip := cm.TipState()
target := tip.MaturityHeight() + 1
for i := tip.Index.Height; i < target; i++ {
b := testutil.MineBlock(cm, types.VoidAddress)
if err := cm.AddBlocks([]types.Block{b}); err != nil {
t.Fatal(err)
}
}

// check that one payout has matured
spendable, confirmed, unconfirmed, err = w.Balance()
if err != nil {
t.Fatal(err)
} else if !confirmed.Equals(initialReward) {
t.Fatalf("expected %v confirmed balance, got %v", initialReward, confirmed)
} else if !spendable.Equals(initialReward) {
t.Fatalf("expected %v spendable balance, got %v", initialReward, spendable)
} else if !unconfirmed.Equals(types.ZeroCurrency) {
t.Fatalf("expected %v unconfirmed balance, got %v", types.ZeroCurrency, unconfirmed)
}

// fund and sign a transaction sending half the balance to the burn address
txn := types.Transaction{
SiacoinOutputs: []types.SiacoinOutput{
{Address: types.VoidAddress, Value: initialReward.Div64(2)},
{Address: w.Address(), Value: initialReward.Div64(2)},
},
}

toSign, release, err := w.FundTransaction(&txn, initialReward, false)
if err != nil {
t.Fatal(err)
}
defer release()
w.SignTransaction(&txn, toSign, types.CoveredFields{WholeTransaction: true})

// check that wallet now has no spendable balance
spendable, confirmed, unconfirmed, err = w.Balance()
if err != nil {
t.Fatal(err)
} else if !confirmed.Equals(initialReward) {
t.Fatalf("expected %v confirmed balance, got %v", initialReward, confirmed)
} else if !spendable.Equals(types.ZeroCurrency) {
t.Fatalf("expected %v spendable balance, got %v", types.ZeroCurrency, spendable)
} else if !unconfirmed.Equals(types.ZeroCurrency) {
t.Fatalf("expected %v unconfirmed balance, got %v", types.ZeroCurrency, unconfirmed)
}

// add the transaction to the pool
if _, err := cm.AddPoolTransactions([]types.Transaction{txn}); err != nil {
t.Fatal(err)
}

// check that the wallet has one unconfirmed transaction
poolTxns, err := w.UnconfirmedTransactions()
if err != nil {
t.Fatal(err)
} else if len(poolTxns) != 1 {
t.Fatal("expected 1 unconfirmed transaction")
}

txn2 := types.Transaction{
SiacoinOutputs: []types.SiacoinOutput{
{Address: types.VoidAddress, Value: initialReward.Div64(2)},
},
}

// try to send a new transaction without using the unconfirmed output
_, _, err = w.FundTransaction(&txn2, initialReward.Div64(2), false)
if !errors.Is(err, wallet.ErrNotEnoughFunds) {
t.Fatalf("expected funding error with no usable utxos, got %v", err)
}

toSign, release, err = w.FundTransaction(&txn2, initialReward.Div64(2), true)
if err != nil {
t.Fatal(err)
}
defer release()
w.SignTransaction(&txn2, toSign, types.CoveredFields{WholeTransaction: true})

// broadcast the transaction
if _, err := cm.AddPoolTransactions([]types.Transaction{txn, txn2}); err != nil {
t.Fatal(err)
}
}

0 comments on commit d0138bd

Please sign in to comment.