diff --git a/chain/manager.go b/chain/manager.go index 25752bd..fe55a7f 100644 --- a/chain/manager.go +++ b/chain/manager.go @@ -928,13 +928,25 @@ func (m *Manager) UnconfirmedParents(txn types.Transaction) []types.Transaction return parents } -// V2UnconfirmedParents returns the v2 transactions in the txpool that are referenced -// by txn. -func (m *Manager) V2UnconfirmedParents(txn types.V2Transaction) []types.V2Transaction { +// V2TransactionSet returns the full transaction set and basis necessary for +// broadcasting a transaction. If the provided basis does not match the current +// tip the transaction will be updated. The transaction set includes the parents +// and the transaction itself in an order valid for broadcasting. +func (m *Manager) V2TransactionSet(basis types.ChainIndex, txn types.V2Transaction) (types.ChainIndex, []types.V2Transaction, error) { m.mu.Lock() defer m.mu.Unlock() m.revalidatePool() + // update the transaction's basis to match tip + _, txns, err := m.updateV2TransactionSet(basis, []types.V2Transaction{txn}) + if err != nil { + return types.ChainIndex{}, nil, fmt.Errorf("failed to update transaction set basis: %w", err) + } else if len(txns) == 0 { + return types.ChainIndex{}, nil, errors.New("no transactions to broadcast") + } + txn = txns[0] + + // get the transaction's parents parentMap := m.computeParentMap() var parents []types.V2Transaction seen := make(map[int]bool) @@ -975,7 +987,7 @@ func (m *Manager) V2UnconfirmedParents(txn types.V2Transaction) []types.V2Transa j := len(parents) - 1 - i parents[i], parents[j] = parents[j], parents[i] } - return parents + return m.tipState.Index, append(parents, txn), nil } func (m *Manager) checkDupTxnSet(txns []types.Transaction, v2txns []types.V2Transaction) (types.Hash256, bool) { @@ -1063,6 +1075,109 @@ func (m *Manager) AddPoolTransactions(txns []types.Transaction) (known bool, err return } +// updateV2TransactionSet updates the basis of a transaction set to the current +// tip. If the basis is already the tip, the transaction set is returned as-is. +// Any transactions that were confirmed are removed from the set. Any ephemeral +// state elements that were created by an update are updated. +// +// If it is undesirable to modify the transaction set, deep-copy it +// before calling this method. +func (m *Manager) updateV2TransactionSet(basis types.ChainIndex, txns []types.V2Transaction) (types.ChainIndex, []types.V2Transaction, error) { + if basis == m.tipState.Index { + return basis, txns, nil + } + + // bring txns up-to-date + revert, apply, err := m.reorgPath(basis, m.tipState.Index) + if err != nil { + return types.ChainIndex{}, nil, fmt.Errorf("couldn't determine reorg path from %v to %v: %w", basis, m.tipState.Index, err) + } else if len(revert)+len(apply) > 144 { + return types.ChainIndex{}, nil, fmt.Errorf("reorg path from %v to %v is too long (-%v +%v)", basis, m.tipState.Index, len(revert), len(apply)) + } + for _, index := range revert { + b, _, cs, ok := blockAndParent(m.store, index.ID) + if !ok { + return types.ChainIndex{}, nil, fmt.Errorf("missing reverted block at index %v", index) + } else if b.V2 == nil { + return types.ChainIndex{}, nil, fmt.Errorf("reorg path from %v to %v contains a non-v2 block (%v)", basis, m.tipState.Index, index) + } + // NOTE: since we are post-hardfork, we don't need a v1 supplement + cru := consensus.RevertBlock(cs, b, consensus.V1BlockSupplement{}) + for i := range txns { + if !updateTxnProofs(&txns[i], cru.UpdateElementProof, cs.Elements.NumLeaves) { + return types.ChainIndex{}, nil, fmt.Errorf("transaction %v references element that does not exist in our chain", txns[i].ID()) + } + } + } + + for _, index := range apply { + b, _, cs, ok := blockAndParent(m.store, index.ID) + if !ok { + return types.ChainIndex{}, nil, fmt.Errorf("missing applied block at index %v", index) + } else if b.V2 == nil { + return types.ChainIndex{}, nil, fmt.Errorf("reorg path from %v to %v contains a non-v2 block (%v)", basis, m.tipState.Index, index) + } + // NOTE: since we are post-hardfork, we don't need a v1 supplement or ancestorTimestamp + cs, cau := consensus.ApplyBlock(cs, b, consensus.V1BlockSupplement{}, time.Time{}) + + // get the transactions that were confirmed in this block + confirmedTxns := make(map[types.TransactionID]bool) + for _, txn := range b.V2Transactions() { + confirmedTxns[txn.ID()] = true + } + confirmedStateElements := make(map[types.Hash256]types.StateElement) + cau.ForEachSiacoinElement(func(sce types.SiacoinElement, created, spent bool) { + if created { + confirmedStateElements[sce.ID] = sce.StateElement + } + }) + cau.ForEachSiafundElement(func(sfe types.SiafundElement, created, spent bool) { + if created { + confirmedStateElements[sfe.ID] = sfe.StateElement + } + }) + + rem := txns[:0] + for i := range txns { + if confirmedTxns[txns[i].ID()] { + // remove any transactions that were confirmed in this block + continue + } + rem = append(rem, txns[i]) + + // update the state elements for any confirmed ephemeral elements + for j := range txns[i].SiacoinInputs { + if txns[i].SiacoinInputs[j].Parent.LeafIndex != types.UnassignedLeafIndex { + continue + } + se, ok := confirmedStateElements[types.Hash256(txns[i].SiacoinInputs[j].Parent.ID)] + if !ok { + continue + } + txns[i].SiacoinInputs[j].Parent.StateElement = se + } + + // update the state elements for any confirmed ephemeral elements + for j := range txns[i].SiafundInputs { + if txns[i].SiafundInputs[j].Parent.LeafIndex != types.UnassignedLeafIndex { + continue + } + se, ok := confirmedStateElements[types.Hash256(txns[i].SiafundInputs[j].Parent.ID)] + if !ok { + continue + } + txns[i].SiafundInputs[j].Parent.StateElement = se + } + + // NOTE: all elements guaranteed to exist from here on, so no + // need to check this return value + updateTxnProofs(&rem[len(rem)-1], cau.UpdateElementProof, cs.Elements.NumLeaves) + } + txns = rem + } + return m.tipState.Index, txns, nil +} + // AddV2PoolTransactions validates a transaction set and adds it to the txpool. // If any transaction references an element (SiacoinOutput, SiafundOutput, or // FileContract) not present in the blockchain, that element must be created by @@ -1093,44 +1208,12 @@ func (m *Manager) AddV2PoolTransactions(basis types.ChainIndex, txns []types.V2T txns[i] = txns[i].DeepCopy() } - if basis != m.tipState.Index { - // bring txns up-to-date - revert, apply, err := m.reorgPath(basis, m.tipState.Index) - if err != nil { - return false, fmt.Errorf("couldn't determine reorg path from %v to %v: %w", basis, m.tipState.Index, err) - } else if len(revert)+len(apply) > 144 { - return false, fmt.Errorf("reorg path from %v to %v is too long (-%v +%v)", basis, m.tipState.Index, len(revert), len(apply)) - } - for _, index := range revert { - b, _, cs, ok := blockAndParent(m.store, index.ID) - if !ok { - return false, fmt.Errorf("missing reverted block at index %v", index) - } else if b.V2 == nil { - return false, m.markBadTxnSet(setID, fmt.Errorf("reorg path from %v to %v contains a non-v2 block (%v)", basis, m.tipState.Index, index)) - } - // NOTE: since we are post-hardfork, we don't need a v1 supplement - cru := consensus.RevertBlock(cs, b, consensus.V1BlockSupplement{}) - for i := range txns { - if !updateTxnProofs(&txns[i], cru.UpdateElementProof, cs.Elements.NumLeaves) { - return false, m.markBadTxnSet(setID, fmt.Errorf("transaction %v references element that does not exist in our chain", txns[i].ID())) - } - } - } - for _, index := range apply { - b, _, cs, ok := blockAndParent(m.store, index.ID) - if !ok { - return false, fmt.Errorf("missing applied block at index %v", index) - } else if b.V2 == nil { - return false, m.markBadTxnSet(setID, fmt.Errorf("reorg path from %v to %v contains a non-v2 block (%v)", basis, m.tipState.Index, index)) - } - // NOTE: since we are post-hardfork, we don't need a v1 supplement or ancestorTimestamp - cs, cau := consensus.ApplyBlock(cs, b, consensus.V1BlockSupplement{}, time.Time{}) - for i := range txns { - // NOTE: all elements guaranteed to exist from here on, so no - // need to check this return value - updateTxnProofs(&txns[i], cau.UpdateElementProof, cs.Elements.NumLeaves) - } - } + // update the transaction set to the current tip + _, txns, err := m.updateV2TransactionSet(basis, txns) + if err != nil { + return false, m.markBadTxnSet(setID, fmt.Errorf("failed to update set basis: %w", err)) + } else if len(txns) == 0 { + return true, nil } // validate as a standalone set diff --git a/wallet/update.go b/wallet/update.go index df2bf29..f19b07b 100644 --- a/wallet/update.go +++ b/wallet/update.go @@ -285,8 +285,11 @@ func appliedEvents(cau chain.ApplyUpdate, walletAddress types.Address) (events [ return } -// applyChainState atomically applies a chain update -func applyChainState(tx UpdateTx, address types.Address, cau chain.ApplyUpdate) error { +// applyChainUpdate atomically applies a chain update +func (sw *SingleAddressWallet) applyChainUpdate(tx UpdateTx, address types.Address, cau chain.ApplyUpdate) error { + sw.mu.Lock() + defer sw.mu.Unlock() + // update current state elements stateElements, err := tx.WalletStateElements() if err != nil { @@ -321,11 +324,15 @@ func applyChainState(tx UpdateTx, address types.Address, cau chain.ApplyUpdate) if err := tx.WalletApplyIndex(cau.State.Index, createdUTXOs, spentUTXOs, appliedEvents(cau, address), cau.Block.Timestamp); err != nil { return fmt.Errorf("failed to apply index: %w", err) } + sw.tip = cau.State.Index return nil } // revertChainUpdate atomically reverts a chain update from a wallet -func revertChainUpdate(tx UpdateTx, revertedIndex types.ChainIndex, address types.Address, cru chain.RevertUpdate) error { +func (sw *SingleAddressWallet) revertChainUpdate(tx UpdateTx, revertedIndex types.ChainIndex, address types.Address, cru chain.RevertUpdate) error { + sw.mu.Lock() + defer sw.mu.Unlock() + var removedUTXOs, unspentUTXOs []types.SiacoinElement cru.ForEachSiacoinElement(func(se types.SiacoinElement, created, spent bool) { switch { @@ -360,6 +367,7 @@ func revertChainUpdate(tx UpdateTx, revertedIndex types.ChainIndex, address type if err := tx.UpdateWalletStateElements(stateElements); err != nil { return fmt.Errorf("failed to update state elements: %w", err) } + sw.tip = revertedIndex return nil } @@ -371,13 +379,15 @@ func (sw *SingleAddressWallet) UpdateChainState(tx UpdateTx, reverted []chain.Re ID: cru.Block.ID(), Height: cru.State.Index.Height + 1, } - if err := revertChainUpdate(tx, revertedIndex, sw.addr, cru); err != nil { - return err + err := sw.revertChainUpdate(tx, revertedIndex, sw.addr, cru) + if err != nil { + return fmt.Errorf("failed to revert chain update %q: %w", cru.State.Index, err) } } for _, cau := range applied { - if err := applyChainState(tx, sw.addr, cau); err != nil { + err := sw.applyChainUpdate(tx, sw.addr, cau) + if err != nil { return fmt.Errorf("failed to apply chain update %q: %w", cau.State.Index, err) } } diff --git a/wallet/wallet.go b/wallet/wallet.go index 04d9da4..33c26be 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -76,7 +76,8 @@ type ( cfg config - mu sync.Mutex // protects the following fields + mu sync.Mutex // protects the following fields + tip types.ChainIndex // locked is a set of siacoin output IDs locked by FundTransaction. They // will be released either by calling Release for unused transactions or // being confirmed in a block. @@ -90,7 +91,6 @@ var ErrDifferentSeed = errors.New("seed differs from wallet seed") // Close closes the wallet func (sw *SingleAddressWallet) Close() error { - // TODO: remove subscription?? return nil } @@ -413,19 +413,18 @@ func (sw *SingleAddressWallet) SignTransaction(txn *types.Transaction, toSign [] // will not be available to future calls to FundTransaction unless ReleaseInputs // is called. // -// The returned consensus state should be used to calculate the input signature -// hash and as the basis for AddV2PoolTransactions. -func (sw *SingleAddressWallet) FundV2Transaction(txn *types.V2Transaction, amount types.Currency, useUnconfirmed bool) (consensus.State, []int, error) { - if amount.IsZero() { - return sw.cm.TipState(), nil, nil - } - +// The returned index should be used as the basis for AddV2PoolTransactions. +func (sw *SingleAddressWallet) FundV2Transaction(txn *types.V2Transaction, amount types.Currency, useUnconfirmed bool) (types.ChainIndex, []int, error) { sw.mu.Lock() defer sw.mu.Unlock() + if amount.IsZero() { + return sw.tip, nil, nil + } + selected, inputSum, err := sw.selectUTXOs(amount, len(txn.SiacoinInputs), useUnconfirmed) if err != nil { - return consensus.State{}, nil, err + return types.ChainIndex{}, nil, err } // add a change output if necessary @@ -445,11 +444,11 @@ func (sw *SingleAddressWallet) FundV2Transaction(txn *types.V2Transaction, amoun sw.locked[sce.ID] = time.Now().Add(sw.cfg.ReservationDuration) } - return sw.cm.TipState(), toSign, nil + return sw.tip, toSign, nil } // SignV2Inputs adds a signature to each of the specified siacoin inputs. -func (sw *SingleAddressWallet) SignV2Inputs(state consensus.State, txn *types.V2Transaction, toSign []int) { +func (sw *SingleAddressWallet) SignV2Inputs(txn *types.V2Transaction, toSign []int) { if len(toSign) == 0 { return } @@ -458,7 +457,7 @@ func (sw *SingleAddressWallet) SignV2Inputs(state consensus.State, txn *types.V2 defer sw.mu.Unlock() policy := sw.SpendPolicy() - sigHash := state.InputSigHash(*txn) + sigHash := sw.cm.TipState().InputSigHash(*txn) for _, i := range toSign { txn.SiacoinInputs[i].SatisfiedPolicy = types.SatisfiedPolicy{ Policy: policy, @@ -468,8 +467,10 @@ func (sw *SingleAddressWallet) SignV2Inputs(state consensus.State, txn *types.V2 } // Tip returns the block height the wallet has scanned to. -func (sw *SingleAddressWallet) Tip() (types.ChainIndex, error) { - return sw.store.Tip() +func (sw *SingleAddressWallet) Tip() types.ChainIndex { + sw.mu.Lock() + defer sw.mu.Unlock() + return sw.tip } // SpendPolicy returns the wallet's default spend policy. @@ -922,6 +923,11 @@ func NewSingleAddressWallet(priv types.PrivateKey, cm ChainManager, store Single opt(&cfg) } + tip, err := store.Tip() + if err != nil { + return nil, fmt.Errorf("failed to get wallet tip: %w", err) + } + sw := &SingleAddressWallet{ priv: priv, @@ -932,6 +938,7 @@ func NewSingleAddressWallet(priv types.PrivateKey, cm ChainManager, store Single log: cfg.Log, addr: types.StandardUnlockHash(priv.PublicKey()), + tip: tip, locked: make(map[types.Hash256]time.Time), } return sw, nil diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go index 314a566..392d2b1 100644 --- a/wallet/wallet_test.go +++ b/wallet/wallet_test.go @@ -596,7 +596,7 @@ func TestWalletRedistributeV2(t *testing.T) { } for i := 0; i < len(txns); i++ { - w.SignV2Inputs(cm.TipState(), &txns[i], toSign[i]) + w.SignV2Inputs(&txns[i], toSign[i]) } if _, err := cm.AddV2PoolTransactions(cm.Tip(), txns); err != nil { return fmt.Errorf("failed to add transactions to pool: %w", err) @@ -1008,14 +1008,14 @@ func TestWalletV2(t *testing.T) { } // fund and sign the transaction - state, toSignV2, err := w.FundV2Transaction(&v2Txn, types.Siacoins(100), false) + basis, toSignV2, err := w.FundV2Transaction(&v2Txn, types.Siacoins(100), false) if err != nil { t.Fatal(err) } - w.SignV2Inputs(state, &v2Txn, toSignV2) + w.SignV2Inputs(&v2Txn, toSignV2) // add the transaction to the pool - if _, err := cm.AddV2PoolTransactions(state.Index, []types.V2Transaction{v2Txn}); err != nil { + if _, err := cm.AddV2PoolTransactions(basis, []types.V2Transaction{v2Txn}); err != nil { t.Fatal(err) } @@ -1139,11 +1139,11 @@ func TestReorgV2(t *testing.T) { } // fund and sign the transaction - state, toSign, err := w.FundV2Transaction(&txn, initialReward, false) + basis, toSign, err := w.FundV2Transaction(&txn, initialReward, false) if err != nil { t.Fatal(err) } - w.SignV2Inputs(state, &txn, toSign) + w.SignV2Inputs(&txn, toSign) // check that wallet now has no spendable balance assertBalance(t, w, types.ZeroCurrency, initialReward, types.ZeroCurrency, types.ZeroCurrency) @@ -1157,7 +1157,7 @@ func TestReorgV2(t *testing.T) { } // add the transaction to the pool - if _, err := cm.AddV2PoolTransactions(state.Index, []types.V2Transaction{txn}); err != nil { + if _, err := cm.AddV2PoolTransactions(basis, []types.V2Transaction{txn}); err != nil { t.Fatal(err) } @@ -1187,11 +1187,11 @@ func TestReorgV2(t *testing.T) { {Address: types.VoidAddress, Value: initialReward}, }, } - state, toSign, err = w.FundV2Transaction(&txn2, initialReward, false) + _, toSign, err = w.FundV2Transaction(&txn2, initialReward, false) if err != nil { t.Fatal(err) } - w.SignV2Inputs(state, &txn2, toSign) + w.SignV2Inputs(&txn2, toSign) // release the inputs to construct a double spend w.ReleaseInputs(nil, []types.V2Transaction{txn2}) @@ -1201,14 +1201,14 @@ func TestReorgV2(t *testing.T) { {Address: types.VoidAddress, Value: initialReward.Div64(2)}, }, } - state, toSign, err = w.FundV2Transaction(&txn1, initialReward.Div64(2), false) + basis, toSign, err = w.FundV2Transaction(&txn1, initialReward.Div64(2), false) if err != nil { t.Fatal(err) } - w.SignV2Inputs(state, &txn1, toSign) + w.SignV2Inputs(&txn1, toSign) // add the first transaction to the pool - if _, err := cm.AddV2PoolTransactions(state.Index, []types.V2Transaction{txn1}); err != nil { + if _, err := cm.AddV2PoolTransactions(basis, []types.V2Transaction{txn1}); err != nil { t.Fatal(err) } mineAndSync(t, cm, ws, w, types.VoidAddress, 1) @@ -1225,7 +1225,7 @@ func TestReorgV2(t *testing.T) { assertBalance(t, w, initialReward.Div64(2), initialReward.Div64(2), types.ZeroCurrency, types.ZeroCurrency) // spend the second transaction to invalidate the confirmed transaction - state = rollbackState + state := rollbackState txn2Height := state.Index.Height + 1 b := types.Block{ ParentID: state.Index.ID, @@ -1343,13 +1343,13 @@ func TestFundTransaction(t *testing.T) { } // Send full confirmed balance to the wallet - state, toSignV2, err := w.FundV2Transaction(&txnV2, sendAmt, false) + basis, toSignV2, err := w.FundV2Transaction(&txnV2, sendAmt, false) if err != nil { t.Fatal(err) } - w.SignV2Inputs(state, &txnV2, toSignV2) + w.SignV2Inputs(&txnV2, toSignV2) - _, err = cm.AddV2PoolTransactions(cm.Tip(), []types.V2Transaction{txnV2}) + _, err = cm.AddV2PoolTransactions(basis, []types.V2Transaction{txnV2}) if err != nil { t.Fatal(err) } @@ -1376,12 +1376,17 @@ func TestFundTransaction(t *testing.T) { }, }, } - state, toSignV2, err = w.FundV2Transaction(&txnV3, sendAmt, true) + basis, toSignV2, err = w.FundV2Transaction(&txnV3, sendAmt, true) if err != nil { t.Fatal(err) } - w.SignV2Inputs(state, &txnV3, toSignV2) - _, err = cm.AddV2PoolTransactions(cm.Tip(), append(cm.V2UnconfirmedParents(txnV3), txnV3)) + w.SignV2Inputs(&txnV3, toSignV2) + basis, txnset, err := cm.V2TransactionSet(basis, txnV3) + if err != nil { + t.Fatal(err) + } + + _, err = cm.AddV2PoolTransactions(basis, txnset) if err != nil { t.Fatal(err) } @@ -1497,10 +1502,10 @@ func TestSingleAddressWalletEventTypes(t *testing.T) { if err != nil { t.Fatal(err) } - wm.SignV2Inputs(basis, &txn, toSign) + wm.SignV2Inputs(&txn, toSign) // broadcast the transaction - if _, err := cm.AddV2PoolTransactions(cm.Tip(), []types.V2Transaction{txn}); err != nil { + if _, err := cm.AddV2PoolTransactions(basis, []types.V2Transaction{txn}); err != nil { t.Fatal(err) } // mine a block to confirm the transaction @@ -1541,10 +1546,10 @@ func TestSingleAddressWalletEventTypes(t *testing.T) { if err != nil { t.Fatal(err) } - wm.SignV2Inputs(basis, &txn, toSign) + wm.SignV2Inputs(&txn, toSign) // broadcast the transaction - if _, err := cm.AddV2PoolTransactions(basis.Index, []types.V2Transaction{txn}); err != nil { + if _, err := cm.AddV2PoolTransactions(basis, []types.V2Transaction{txn}); err != nil { t.Fatal(err) } // current tip @@ -1617,10 +1622,10 @@ func TestSingleAddressWalletEventTypes(t *testing.T) { if err != nil { t.Fatal(err) } - wm.SignV2Inputs(basis, &txn, toSign) + wm.SignV2Inputs(&txn, toSign) // broadcast the transaction - if _, err := cm.AddV2PoolTransactions(basis.Index, []types.V2Transaction{txn}); err != nil { + if _, err := cm.AddV2PoolTransactions(basis, []types.V2Transaction{txn}); err != nil { t.Fatal(err) } // current tip @@ -1700,10 +1705,10 @@ func TestSingleAddressWalletEventTypes(t *testing.T) { if err != nil { t.Fatal(err) } - wm.SignV2Inputs(basis, &txn, toSign) + wm.SignV2Inputs(&txn, toSign) // broadcast the transaction - if _, err := cm.AddV2PoolTransactions(cm.Tip(), []types.V2Transaction{txn}); err != nil { + if _, err := cm.AddV2PoolTransactions(basis, []types.V2Transaction{txn}); err != nil { t.Fatal(err) } // current tip @@ -1762,7 +1767,7 @@ func TestSingleAddressWalletEventTypes(t *testing.T) { if err != nil { t.Fatal(err) } - wm.SignV2Inputs(setupBasis, &setupTxn, setupToSign) + wm.SignV2Inputs(&setupTxn, setupToSign) // create the renewal transaction resolutionTxn := types.V2Transaction{ @@ -1781,10 +1786,10 @@ func TestSingleAddressWalletEventTypes(t *testing.T) { }, }, } - wm.SignV2Inputs(setupBasis, &resolutionTxn, []int{0}) + wm.SignV2Inputs(&resolutionTxn, []int{0}) // broadcast the renewal - if _, err := cm.AddV2PoolTransactions(cm.Tip(), []types.V2Transaction{setupTxn, resolutionTxn}); err != nil { + if _, err := cm.AddV2PoolTransactions(setupBasis, []types.V2Transaction{setupTxn, resolutionTxn}); err != nil { t.Fatal(err) } // mine a block to confirm the renewal @@ -1824,10 +1829,10 @@ func TestSingleAddressWalletEventTypes(t *testing.T) { if err != nil { t.Fatal(err) } - wm.SignV2Inputs(basis, &txn, toSign) + wm.SignV2Inputs(&txn, toSign) // broadcast the transaction - if _, err := cm.AddV2PoolTransactions(cm.Tip(), []types.V2Transaction{txn}); err != nil { + if _, err := cm.AddV2PoolTransactions(basis, []types.V2Transaction{txn}); err != nil { t.Fatal(err) } // current tip @@ -1879,3 +1884,79 @@ func TestSingleAddressWalletEventTypes(t *testing.T) { assertEvent(t, wm, types.Hash256(types.FileContractID(fce.ID).V2RenterOutputID()), wallet.EventTypeV2ContractResolution, renterPayout, types.ZeroCurrency, cm.Tip().Height+144) }) } + +func TestV2TPoolRace(t *testing.T) { + // create wallet store + pk := types.GeneratePrivateKey() + ws := testutil.NewEphemeralWalletStore() + + // create chain store + network, genesis := testutil.V2Network() + cs, genesisState, err := chain.NewDBStore(chain.NewMemDB(), network, genesis) + if err != nil { + t.Fatal(err) + } + + // create chain manager and subscribe the wallet + cm := chain.NewManager(cs, genesisState) + // create wallet + l := zaptest.NewLogger(t) + w, err := wallet.NewSingleAddressWallet(pk, cm, ws, wallet.WithLogger(l.Named("wallet"))) + if err != nil { + t.Fatal(err) + } + defer w.Close() + + // fund the wallet + mineAndSync(t, cm, ws, w, w.Address(), 1) + // mine until one utxo is mature + mineAndSync(t, cm, ws, w, types.VoidAddress, 144) + + // create a transaction that creates an ephemeral output with 1000 SC + setupTxn := types.V2Transaction{ + SiacoinOutputs: []types.SiacoinOutput{ + {Address: w.Address(), Value: types.Siacoins(1000)}, + }, + } + basis, toSign, err := w.FundV2Transaction(&setupTxn, types.Siacoins(1000), false) + if err != nil { + t.Fatal(err) + } + w.SignV2Inputs(&setupTxn, toSign) + + // broadcast the setup transaction + if _, err := cm.AddV2PoolTransactions(basis, []types.V2Transaction{setupTxn}); err != nil { + t.Fatal(err) + } + + // create a transaction that spends the ephemeral output + spendTxn := types.V2Transaction{ + SiacoinOutputs: []types.SiacoinOutput{ + {Address: types.VoidAddress, Value: types.Siacoins(1000)}, + }, + } + + // try to fund with non-ephemeral output, should fail + if _, _, err = w.FundV2Transaction(&spendTxn, types.Siacoins(1000), false); err == nil { + t.Fatal("expected funding error, got nil") + } + + // fund with the tpool ephemeral output + basis, toSign, err = w.FundV2Transaction(&spendTxn, types.Siacoins(1000), true) + if err != nil { + t.Fatal(err) + } + w.SignV2Inputs(&spendTxn, toSign) + + // mine to confirm the setup transaction. This will make the ephemeral + // output in the spend transaction invalid unless it is updated. + mineAndSync(t, cm, ws, w, types.VoidAddress, 1) + + // broadcast the transaction set including the already confirmed setup + // transaction. This seems unnecessary, but it's a fairly common occurrence + // when passing transaction sets using unconfirmed outputs between a renter + // and host. If the transaction set is not updated correctly, it will fail. + if _, err := cm.AddV2PoolTransactions(basis, []types.V2Transaction{setupTxn, spendTxn}); err != nil { + t.Fatal(err) + } +}