diff --git a/itest/assertions.go b/itest/assertions.go index 5828c4365..ed5d584fb 100644 --- a/itest/assertions.go +++ b/itest/assertions.go @@ -963,7 +963,7 @@ func AssertSendEventsComplete(t *testing.T, scriptKey []byte, stream *EventSubscription[*taprpc.SendEvent]) { AssertSendEvents( - t, scriptKey, stream, tapfreighter.SendStateWaitTxConf, + t, scriptKey, stream, tapfreighter.SendStateBroadcastComplete, tapfreighter.SendStateComplete, ) } diff --git a/itest/psbt_test.go b/itest/psbt_test.go index 00512b8b4..6104c355f 100644 --- a/itest/psbt_test.go +++ b/itest/psbt_test.go @@ -1388,7 +1388,7 @@ func testPsbtMultiSend(t *harnessTest) { AssertSendEvents( t.t, scriptKey1Bytes, sendEvents, - tapfreighter.SendStateWaitTxConf, + tapfreighter.SendStateBroadcastComplete, tapfreighter.SendStateComplete, ) diff --git a/tapfreighter/chain_porter.go b/tapfreighter/chain_porter.go index e58bfe671..db51bae34 100644 --- a/tapfreighter/chain_porter.go +++ b/tapfreighter/chain_porter.go @@ -1178,6 +1178,9 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) { "minimum relay fee: %w", err) } + log.Infof("Min relay fee: %v", + minRelayFee.FeePerKVByte().String()) + // If the fee rate is below the minimum relay fee, we'll // bump it up. if feeRate < minRelayFee { @@ -1388,10 +1391,32 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) { "transaction %v: %w", txHash, err) case err != nil: + // If the error is due to the min relay fee not being + // met, we'll unlock the inputs we locked for this + // transfer before returning the error. + if strings.Contains( + err.Error(), "min relay fee not met", + ) { + p.unlockInputs(ctx, ¤tPkg) + } + + // We exercise caution by not unlocking the inputs in + // the general error case, in case the transaction was + // somehow broadcasted. return nil, fmt.Errorf("unable to broadcast "+ "transaction %v: %w", txHash, err) } + // Set send state to the next state to evaluate. + currentPkg.SendState = SendStateBroadcastComplete + return ¤tPkg, nil + + // At this stage, the transaction has been broadcast to the network. + // From this point forward, the transfer cancellation methodology + // changes. + case SendStateBroadcastComplete: + log.Infof("Transfer tx broadcast complete") + // With the transaction broadcast, we'll deliver a // notification via the transaction broadcast response channel. currentPkg.deliverTxBroadcastResp() @@ -1466,6 +1491,9 @@ func (p *ChainPorter) unlockInputs(ctx context.Context, pkg *sendPackage) { return } + // TODO(ffranr): Make use of CheckMempoolAccept to ensure we don't + // unlock inputs for transactions that are in the mempool. + // If we haven't even attempted to broadcast yet, we're still in a state // where we give feedback to the user synchronously, as we haven't // created an on-chain transaction that we need to await confirmation. @@ -1474,7 +1502,7 @@ func (p *ChainPorter) unlockInputs(ctx context.Context, pkg *sendPackage) { // sanity-check that we have known input commitments to unlock, since // that might not always be the case (for example if another party // contributes inputs). - if pkg.SendState < SendStateStorePreBroadcast && + if pkg.SendState < SendStateBroadcastComplete && len(pkg.InputCommitments) > 0 { for prevID := range pkg.InputCommitments { @@ -1505,6 +1533,9 @@ func (p *ChainPorter) unlockInputs(ctx context.Context, pkg *sendPackage) { log.Warnf("Unable to unlock input %v: %v", op, err) } } + + // TODO(ffranr): Remove pending asset transfer and chain tx from the + // database. } // logPacket logs the virtual packet to the debug log. diff --git a/tapfreighter/parcel.go b/tapfreighter/parcel.go index efe5f9157..82115fd65 100644 --- a/tapfreighter/parcel.go +++ b/tapfreighter/parcel.go @@ -47,6 +47,12 @@ const ( // ensure it properly tracks the coins allocated to the anchor output. SendStateBroadcast + // SendStateBroadcastComplete represents the state where the transfer + // transaction has been broadcast and is either in the mempool or + // confirmed on-chain. At this stage, cancellation cannot rely solely + // on naive coin unlocking. + SendStateBroadcastComplete + // SendStateWaitTxConf is a state in which we will wait for the transfer // transaction to confirm on-chain. SendStateWaitTxConf @@ -85,6 +91,9 @@ func (s SendState) String() string { case SendStateBroadcast: return "SendStateBroadcast" + case SendStateBroadcastComplete: + return "SendStateBroadcastComplete" + case SendStateWaitTxConf: return "SendStateWaitTxConf" diff --git a/tapfreighter/wallet.go b/tapfreighter/wallet.go index baff3a3bb..63f822da4 100644 --- a/tapfreighter/wallet.go +++ b/tapfreighter/wallet.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/mempool" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" @@ -1438,11 +1439,21 @@ func (f *AssetWallet) AnchorVirtualTransactions(ctx context.Context, } // Final TX sanity check. - err = blockchain.CheckTransactionSanity(btcutil.NewTx(finalTx)) + finalBtcUtilTx := btcutil.NewTx(finalTx) + err = blockchain.CheckTransactionSanity(finalBtcUtilTx) if err != nil { return nil, fmt.Errorf("anchor TX failed final checks: %w", err) } + // Report the actual fee rate that will be paid. + // + // Compute the virtual size of the transaction in bytes. + size := mempool.GetTxVirtualSize(finalBtcUtilTx) + + // Compute the fee rate in sat/kvb. + actualFeeRate := int64(chainFees) * 1000 / size + log.Infof("Anchor TX final fee rate: %d sat/kvb", actualFeeRate) + anchorTx := &tapsend.AnchorTransaction{ FundedPsbt: anchorPkt, FinalTx: finalTx,