Skip to content
This repository has been archived by the owner on Nov 2, 2018. It is now read-only.

Make wallet rebroadcast transactions until they are confirmed #2611

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

ChrisSchinnerl
Copy link
Contributor

This is a work in progress but I would appreciate some code review before I continue this.


// rebroadcastMaxTries is the maximum number of times a transaction set
// will be rebroadcasted before the wallet stops tracking it
rebroadcastMaxTries = 10
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a const called RespendTimeout in wallet.go (that should be moved to consts.go) which says the wallet will try to spend an output again after 40 blocks of it not being spent. Instead of having rebroadcastMaxTries, you should just stop trying once the respend timeout has expired.

Now that we are trying to rebroadcast transactions, we can probably jump the RespendTimeout to 72 blocks, which is 12 hours.

@DavidVorick
Copy link
Member

Part of the purpose of this PR is to make sure transactions get re-broadcast. Right now, I'm pretty sure that if you call tpool.AcceptTransactionSet with a transaction set that you've already added to the tpool, you will get an error, and it won't broadcast.

So we should modify the AcceptTransactionSet to go ahead and broadcast the transaction and reset the timer on the transactions (they get booted after 12 blocks) if the broadcast function is called again.

I guess it's a little complicated though, because the wallet is going to start respending the transactions at some point, and we need to make sure that by that point the previous transaction in the transaction pool has been properly booted. I guess we'll need to leverage some consts in the modules package.

for _, sco := range txn.SiacoinOutputs {
relevant = relevant || w.isWalletAddress(sco.UnlockHash)
}
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just throwing it out there to put it on the radar, it's not important for this PR, but the wallet is going to need to start recognizing file contracts as well as siacoin + siafund transactions.

tries int
confirmedTxn map[types.TransactionID]bool
transactions []types.Transaction
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably move this code into broadcast.go or something. I think there's enough logic here that it deserves its own file.

@DavidVorick
Copy link
Member

I think generally speaking this code is on the right track. The one thing that seems to be missing is persistence - if you create a transaction, you should persist it in the database, so that if you start the wallet again later, it can be re-broadcast again. Have to be careful about that though, because after RespendTimeout the wallet will start using those outputs to make other transactions again, and we want to make sure we aren't broadcasting transactions which the wallet will try to spend again.

@ChrisSchinnerl ChrisSchinnerl force-pushed the wallet-rebroadcast branch 4 times, most recently from 87422ae to 29c5100 Compare January 5, 2018 14:42
@4Dolio
Copy link

4Dolio commented Jan 5, 2018

Great issue! Just throwing it out there, wr could use a traditional 'progressive' retry? Begin to retry in a short block height and get slower. Not sure is a very short retry would induce problems or cause a feedback overload to the blockchain/network itself? So pick a reasonably fast yet safe start duration. Say 5, then 10, 20, 40, 80, 160, untill acceptable max like 1 month? Week?

// will boot transactions after MaxTxnAge. We need to make sure that we
// leave at least MaxTxnAge blocks after the last broadcast to allow for
// the transasction to be pruned before the wallet tries to respend it.
rebroadcastTimeout = types.BlockHeight(respendTimeout - modules.MaxTxnAge)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

excellent comments

@DavidVorick
Copy link
Member

Well, it's bad for the network to broadcast frequently. And, if you haven't broadcasted in 24 blocks, your transactions will have been dropped from any nodes running standard code, and will be unlikely to reach miners. So at the very least, you'd want to try once every 24 blocks I'd think.

But, once every 6 blocks seems pretty sound / okay as well, at least for now. If transaction pools are full or the fees are too high, it'll likely change after 6 blocks.

@jarcko
Copy link

jarcko commented Jan 11, 2018

Tranascations not found here
https://explore.sia.tech/hashes/50a860e7e4ce6976710d1d52b580bb83ca5e125dbbfd5e34722811a365676a05

screen shot 2018-01-11 at 7 54 55 pm

I see it every time so can't send coins at all, I reinstalled all from scratch and recovered the wallet, but still the same. Can you reproduce the issue?

@DavidVorick
Copy link
Member

Are you running a host? Did you just make that transaction? How long did you wait for the coins to send?

const (
// maxTxnAge determines the maximum age of a transaction (in block height)
// allowed before the transaction is pruned from the transaction pool.
MaxTxnAge = types.BlockHeight(24)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that there are other consts throughout the modules package, we may want to collect them all here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should that be done in the same PR?

tp.transactionHeights[txn.ID()] = tp.blockHeight
}
}
go tp.gateway.Broadcast("RelayTransactionSet", ts, tp.gateway.Peers())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calling tp.gateway.Peers() while the tpool is under lock is a deadlock risk, need to restructure to avoid this (looks like the existing code had this deadlock risk in it as well)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general convention is that a module can't call exported methods of other modules while under lock


// confirmed is a helper function that sets a certain transactions to confirmed
// or unconfirmed. It also updates the state on disk.
func (bts *broadcastedTSet) confirmed(txid types.TransactionID, confirmed bool) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function name suggests a query, as though you are asking for the confirmed status of a transaction. Should change the name to 'markConfirmation' or something similar.


// rebroadcastOldTransaction rebroadcasts transactions that haven't been
// confirmed within rebroadcastInterval blocks
func (w *Wallet) rebroadcastOldTransactions(tx *bolt.Tx, cc modules.ConsensusChange) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should probably be moved to broadcast.go


// Mark reverted transactions as not confirmed
for _, block := range cc.RevertedBlocks {
for _, bts := range w.broadcastedTSets {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an n^2 algorithm. The thing that worries me is that for each broadcastedTSet, you are iterating through every transaction of every block. If a wallet has worked up something like 100 broadcastedTSets over the past few blocks (not unreasonable for an exchange with lots of inbound attention), and there are 2000 transactions per block, we could be seeing 200,000 operations happening to get through each block. Combine that with a large (say 5 blocks) reorg and you could be seeing 4 reversions + 5 additions = 1.8 million total operations.

Need to find a faster way to do this.

if _, exists := bts.confirmedTxn[txn.ID()]; exists {
err = bts.confirmed(txn.ID(), false)
}
if err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error handling needs to happen in the same scope as error creation

@@ -331,18 +331,33 @@ func (tp *TransactionPool) AcceptTransactionSet(ts []types.Transaction) error {
return errors.New("consensus set does not support LockedTryTransactionSet method")
}

return cs.LockedTryTransactionSet(func(txnFn func(txns []types.Transaction) (modules.ConsensusChange, error)) error {
err := cs.LockedTryTransactionSet(func(txnFn func(txns []types.Transaction) (modules.ConsensusChange, error)) error {
tp.mu.Lock()
defer tp.mu.Unlock()
err := tp.acceptTransactionSet(ts, txnFn)
if err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if err == nil, right now you are only updating subscribers if there is an error :P

for _, txn := range ts {
tp.transactionHeights[txn.ID()] = tp.blockHeight
}
tp.mu.Unlock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I missed this. This should happen under the same lock that you update the subscribers with.

// blocks
if consensusHeight >= bts.firstTry+RebroadcastTimeout {
if err := w.deleteBroadcastedTSet(tSetID); err != nil {
return err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably better to log the error than to return in this case.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants