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

client/asset/btc: work around PublishTransaction delays #1376

Merged
merged 1 commit into from
Jan 13, 2022
Merged
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
74 changes: 57 additions & 17 deletions client/asset/btc/spv.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ const (
WalletTransactionNotFound = dex.ErrorKind("wallet transaction not found")
SpentStatusUnknown = dex.ErrorKind("spend status not known")

// defaultBroadcastWait is long enough for btcwallet's PublishTransaction
// method to record the outgoing transaction and queue it for broadcasting.
// This rough duration is necessary since with neutrino as the wallet's
// chain service, its chainClient.SendRawTransaction call is blocking for up
// to neutrino.Config.BroadcastTimeout while peers either respond to the inv
// request with a getdata or time out. However, in virtually all cases, we
// just need to know that btcwallet was able to create and store the
// transaction record, and pass it to the chain service.
defaultBroadcastWait = 2 * time.Second

// see btcd/blockchain/validate.go
maxFutureBlockTime = 2 * time.Hour
neutrinoDBName = "neutrino.db"
Expand Down Expand Up @@ -398,20 +408,45 @@ func (w *spvWallet) ownsAddress(addr btcutil.Address) (bool, error) {
}

func (w *spvWallet) sendRawTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) {
// Fee sanity check?
err := w.wallet.PublishTransaction(tx, "")
if err != nil {
return nil, err
}
txHash := tx.TxHash()
// Publish the transaction in a goroutine so the caller may wait for a given
// period before it goes asynchronous and it is assumed that btcwallet at
// least succeeded with its DB updates and queueing of the transaction for
// rebroadcasting. In the future, a new btcwallet method should be added
// that returns after performing its internal actions, but broadcasting
// asynchronously and sending the outcome in a channel or promise.
Comment on lines +414 to +416
Copy link
Member Author

@chappjc chappjc Jan 12, 2022

Choose a reason for hiding this comment

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

I hate the guesstimation in this PR using 2 seconds to let it process and queue the txn when 100 ms is more than adequate most of the time, but the proper solution is described on these lines (a different btcwallet method).

res := make(chan error, 1)
go func() {
tStart := time.Now()
defer close(res)
if err := w.wallet.PublishTransaction(tx, ""); err != nil {
w.log.Errorf("PublishTransaction(%v) failure: %v", tx.TxHash(), err)
res <- err
return
}
defer w.log.Tracef("PublishTransaction(%v) completed in %v", tx.TxHash(),
time.Since(tStart)) // after outpoint unlocking and signalling

// bitcoind would unlock these, but it seems that btcwallet does not.
// However, it seems like they are no longer returned from ListUnspent
// even if we unlock the outpoint before the transaction is mined, so
// this is just housekeeping for btcwallet's lockedOutpoints map.
for _, txIn := range tx.TxIn {
w.wallet.UnlockOutpoint(txIn.PreviousOutPoint)
}
res <- nil
}()

// bitcoind would unlock these, but it seems that btcwallet doesn't, but it
// seems like they're no longer returned from ListUnspent even if we unlock
// the outpoint before the transaction is mined.
for _, txIn := range tx.TxIn {
w.wallet.UnlockOutpoint(txIn.PreviousOutPoint)
select {
case err := <-res:
if err != nil {
return nil, err
}
case <-time.After(defaultBroadcastWait):
w.log.Debugf("No error from PublishTransaction after %v for txn %v. "+
"Assuming wallet accepted it.", defaultBroadcastWait, tx.TxHash())
}

txHash := tx.TxHash() // down here in case... the msgTx was mutated?
return &txHash, nil
}

Expand Down Expand Up @@ -1160,12 +1195,17 @@ func (w *spvWallet) startWallet() error {
}
}
chainService, err := neutrino.NewChainService(neutrino.Config{
DataDir: w.netDir,
Database: w.neutrinoDB,
ChainParams: *w.chainParams,
AddPeers: addPeers,
ConnectPeers: w.connectPeers,
BroadcastTimeout: 10 * time.Second,
DataDir: w.netDir,
Database: w.neutrinoDB,
ChainParams: *w.chainParams,
PersistToDisk: true, // keep cfilter headers on disk for efficient rescanning
AddPeers: addPeers,
ConnectPeers: w.connectPeers,
// WARNING: PublishTransaction currently uses the entire duration
// because if an external bug, but even if the resolved, a typical
// inv/getdata round trip is ~4 seconds, so we set this so neutrino does
// not cancel queries too readily.
BroadcastTimeout: 6 * time.Second,
Copy link
Member

@buck54321 buck54321 Jan 12, 2022

Choose a reason for hiding this comment

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

Looks like the default is 5? So we're actually giving neutrino more time, just not waiting around anymore.

Copy link
Member Author

@chappjc chappjc Jan 12, 2022

Choose a reason for hiding this comment

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

Correct. So 10 or even 30 would be fine with this approach, but I figured I would set it to something based on my empirical observations now that I've studied it more in practice. 5 seemed just under the wire to me.

})
if err != nil {
bailOnWalletAndDB()
Expand Down