diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index bc7ff8a829..717f96fbb2 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -291,22 +291,35 @@ func RPCConfigOpts(name, rpcPort string) []*asset.ConfigOption { } } +// OrderEstimator estimates the funding required for an order. A clone may +// specify a custom OrderEstimator via their BTCCloneCFG. Otherwise +// defaultOrderEstimator (calc.RequiredOrderFundsAlt) is used. type OrderEstimator func(swapVal, inputCount, inputsSize, maxSwaps, swapSizeBase, swapSize, feeRate uint64) uint64 func defaultOrderEstimator(swapVal, _, inputsSize, maxSwaps, swapSizeBase, swapSize, feeRate uint64) uint64 { return calc.RequiredOrderFundsAlt(swapVal, inputsSize, maxSwaps, swapSizeBase, swapSize, feeRate) } +// DustRater indicates whether an output is considered dust. A clone may specify +// a custom DustRater via their BTCCloneCFG. Otherwise, dexbtc.IsDustVal will +// be used. type DustRater func(txSize, value, minRelayTxFee uint64, segwit bool) bool var defaultDustRater = dexbtc.IsDustVal +// TxFeesCalculator calculates the fees for a transaction. The baseTxSize +// argument should be the tx size including outputs but without inputs. A clone +// may specify a custom TxFeesCalculator via their BTCCloneCFG. Otherwise, +// defaultTxFeesCalculator is used. type TxFeesCalculator func(baseTxSize, inputCount, inputsSize, feeRate uint64) uint64 func defaultTxFeesCalculator(baseTxSize, _, inputsSize, feeRate uint64) uint64 { return (baseTxSize + inputsSize) * feeRate } +// SplitFeeCalculator calculates the fees associated with a split tx. A clone +// may specify a custom SplitFeeCalculator via their BTCCloneCFG. Otherwise, +// defaultSplitFeeCalculator is used. type SplitFeeCalculator func(inputCount, inputsSize, maxFeeRate uint64, extraOutput, segwit bool) (swapInputSize, baggage uint64) func defaultSplitFeeCalculator(_, _, maxFeeRate uint64, extraOutput bool, segwit bool) (swapInputSize, baggage uint64) { @@ -418,10 +431,20 @@ type BTCCloneCFG struct { TxHasher func(*wire.MsgTx) *chainhash.Hash // TxSizeCalculator is an optional function that will be used to calculate // the size of a transaction. - TxSizeCalculator func(*wire.MsgTx) uint64 - DustRater DustRater - OrderEstimator OrderEstimator - TxFeesCalculator TxFeesCalculator + TxSizeCalculator func(*wire.MsgTx) uint64 + // DustRater is an optional function to calculate whether a tx output is + // dust. If not specified, dexbtc.IsDustVal will be used. + DustRater DustRater + // OrderEstimator is an optional function that will calculate the funding + // required for an order. In not spcified, calc.RequiredOrderFunds will be + // used. + OrderEstimator OrderEstimator + // TxFeesCalculator is an optional function that calculates fees for a tx. + // If not specified, the typical tx_size * fee_rate formula will be used. + TxFeesCalculator TxFeesCalculator + // SplitFeeCalculator is an optional function that specified the fees + // associated with a split tx. If not specified, a typical + // tx_size * fee_rate formula is used. SplitFeeCalculator SplitFeeCalculator // TxVersion is an optional function that returns a version to use for // new transactions. @@ -4848,22 +4871,19 @@ func (btc *baseWallet) signTxAndAddChange(baseTx *wire.MsgTx, addr btcutil.Addre } vSize := btc.calcTxSize(msgTx) - sumInputSizes := func(msgTx *wire.MsgTx) (inputsSize uint64) { - for _, txIn := range msgTx.TxIn { - if len(txIn.Witness) > 0 { - const witnessWeight = 4 - witnessSize := uint64(txIn.Witness.SerializeSize()) - inputsSize += dexbtc.TxInOverhead + (witnessSize+(witnessWeight-1))/witnessWeight - } else { - inputsSize += uint64(txIn.SerializeSize()) - } + inCount := uint64(len(msgTx.TxIn)) + var inputsSize uint64 + for _, txIn := range msgTx.TxIn { + if len(txIn.Witness) > 0 { + const witnessWeight = 4 + witnessSize := uint64(txIn.Witness.SerializeSize()) + inputsSize += dexbtc.TxInOverhead + (witnessSize+(witnessWeight-1))/witnessWeight + } else { + inputsSize += uint64(txIn.SerializeSize()) } - return } - inCount := uint64(len(msgTx.TxIn)) - inputsSize := sumInputSizes(msgTx) - baseTxSizeWithoutInputs := vSize - inputsSize + baseTxSizeWithoutInputs := vSize - inputsSize minFee := btc.txFees(baseTxSizeWithoutInputs, inCount, inputsSize, feeRate) // feeRate * vSize remaining := totalIn - totalOut if minFee > remaining { @@ -4901,7 +4921,6 @@ func (btc *baseWallet) signTxAndAddChange(baseTx *wire.MsgTx, addr btcutil.Addre btc.log.Debugf("Change output size = %d, addr = %s", changeSize, addrStr) vSize += changeSize - inputsSize := sumInputSizes(msgTx) fee := btc.txFees(vSize-inputsSize, inCount, inputsSize, feeRate) changeOutput.Value = int64(remaining - fee) // Find the best fee rate by closing in on it in a loop. @@ -4914,7 +4933,6 @@ func (btc *baseWallet) signTxAndAddChange(baseTx *wire.MsgTx, addr btcutil.Addre return makeErr("signing error: %v, raw tx: %x", err, btc.wireBytes(baseTx)) } vSize = btc.calcTxSize(msgTx) // recompute the size with new tx signature - inputsSize := sumInputSizes(msgTx) reqFee := btc.txFees(vSize-inputsSize, inCount, inputsSize, feeRate) if reqFee > remaining { // I can't imagine a scenario where this condition would be true, but @@ -5077,6 +5095,8 @@ func (btc *baseWallet) EstimateSendTxFee(address string, sendAmount, feeRate uin tx := wire.NewMsgTx(btc.txVersion()) tx.AddTxOut(wireOP) + // If the node has a better way, let them do it. Otherwise, we'll use our + // own method, which pulls all unspent outputs. estimator, is := btc.node.(txFeeEstimator) if btc.manualSendEst || !is { estimator = btc diff --git a/client/asset/btc/simnet_test.go b/client/asset/btc/simnet_test.go index 1b2d7a9c0c..0279730fac 100644 --- a/client/asset/btc/simnet_test.go +++ b/client/asset/btc/simnet_test.go @@ -241,11 +241,21 @@ func TestMakeBondTx(t *testing.T) { t.Logf("refundCoin: %v\n", refundCoin) } -func TestSendEstimation(t *testing.T) { +// TestCompareSendFeeEstimation compares our manual funding routine, which pulls +// all unspent outputs, to the rpcClient method that uses the rundrawtransaction +// rpc. +func TestCompareSendFeeEstimation(t *testing.T) { rig := newTestRig(t, func(name string, err error) { tLogger.Infof("%s has reported a new block, error = %v", name, err) }) - defer rig.close(t) + const numCycles = 10 + var manualTime, rpcTime time.Duration + defer func() { + rig.close(t) + fmt.Printf("%d cycles each\n", numCycles) + fmt.Println("Time to pick utxos ourselves:", manualTime) + fmt.Println("Time to use fundrawtransaction:", rpcTime) + }() addr, _ := btcutil.DecodeAddress("bcrt1qs6d2lpkcfccus6q7c0dvjnlpf5g45gf7yak6mm", &chaincfg.RegressionNetParams) pkScript, _ := txscript.PayToAddrScript(addr) @@ -254,7 +264,6 @@ func TestSendEstimation(t *testing.T) { // Use alpha, since there are many utxos. w := rig.alpha() - const numCycles = 100 tStart := time.Now() for i := 0; i < numCycles; i++ { _, err := w.estimateSendTxFee(tx, 20, false) @@ -262,7 +271,8 @@ func TestSendEstimation(t *testing.T) { t.Fatalf("Error estimating with utxos: %v", err) } } - fmt.Println("Time to pick utxos ourselves:", time.Since(tStart)) + manualTime = time.Since(tStart) + node := w.node.(*rpcClient) tStart = time.Now() for i := 0; i < numCycles; i++ { @@ -271,5 +281,5 @@ func TestSendEstimation(t *testing.T) { t.Fatalf("Error estimating with utxos: %v", err) } } - fmt.Println("Time to use fundrawtransaction:", time.Since(tStart)) + rpcTime = time.Since(tStart) } diff --git a/client/asset/zec/zec.go b/client/asset/zec/zec.go index 0a1f0986ba..82bff76b73 100644 --- a/client/asset/zec/zec.go +++ b/client/asset/zec/zec.go @@ -216,8 +216,10 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, net dex.Network) (ass TxFeesCalculator: func(baseTxSize, inputCount, inputsSize, _ uint64) uint64 { return txFees(baseTxSize, inputCount, inputsSize) }, - SplitFeeCalculator: splitTxFees, - NonSegwitSigner: signTx, + SplitFeeCalculator: func(inputCount, inputsSize, _ uint64, extraOutput, _ bool) (swapInputSize, baggage uint64) { + return splitTxFees(inputCount, inputsSize, extraOutput) + }, + NonSegwitSigner: signTx, TxDeserializer: func(b []byte) (*wire.MsgTx, error) { zecTx, err := dexzec.DeserializeTx(b) if err != nil { @@ -294,10 +296,10 @@ func (w *zecWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) { func (w *zecWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, error) { var singleTxInsSize uint64 = dexbtc.TxInOverhead + dexbtc.RedeemSwapSigScriptSize + 1 var txOutsSize uint64 = dexbtc.P2PKHOutputSize + 1 - worstCaseFees := dexzec.TransparentTxFeesZIP317(singleTxInsSize, txOutsSize) * req.Lots - - multiTxInsSize := dexbtc.TxInOverhead + dexbtc.RedeemSwapSigScriptSize*req.Lots + uint64(wire.VarIntSerializeSize(req.Lots)) - bestCaseFees := dexzec.TransparentTxFeesZIP317(multiTxInsSize, txOutsSize) * req.Lots + // best case: all lots in a single match. + bestCaseFees := dexzec.TransparentTxFeesZIP317(singleTxInsSize, txOutsSize) + // worst-case: all single-lot matches no batching. + worstCaseFees := bestCaseFees * req.Lots return &asset.PreRedeem{ Estimate: &asset.RedeemEstimate{ RealisticWorstCase: worstCaseFees, @@ -649,7 +651,8 @@ func isDust(val, outputSize uint64) bool { var emptyTxSize = dexzec.CalcTxSize(new(wire.MsgTx)) -// txFees is the tx fees for a basic single-output send with change. +// txFees calculates tx fees. baseTxSize is the size of the transaction with +// outputs and no inputs. func txFees(baseTxSize, inputCount, inputsSize uint64) uint64 { txInSize := inputsSize + uint64(wire.VarIntSerializeSize(inputCount)) outputsSize := baseTxSize - emptyTxSize @@ -657,7 +660,8 @@ func txFees(baseTxSize, inputCount, inputsSize uint64) uint64 { return dexzec.TransparentTxFeesZIP317(txInSize, txOutSize) } -func splitTxFees(inputCount, inputsSize, _ uint64, extraOutput, _ bool) (swapInputSize, baggage uint64) { +// splitTxFees calculates the fees for a split tx. +func splitTxFees(inputCount, inputsSize uint64, extraOutput bool) (swapInputSize, baggage uint64) { txInsSize := inputsSize + uint64(wire.VarIntSerializeSize(inputCount)) var numOutputs uint64 = 2 if extraOutput { diff --git a/dex/networks/zec/script.go b/dex/networks/zec/script.go index c3345fb904..e3cde10810 100644 --- a/dex/networks/zec/script.go +++ b/dex/networks/zec/script.go @@ -10,10 +10,11 @@ import ( "github.com/btcsuite/btcd/wire" ) +// https://zips.z.cash/zip-0317 + // TransparentTxFeesZIP317 calculates the ZIP-0317 fees for a fully transparent // Zcash transaction, which only depends on the size of the tx_in and tx_out // fields. -// https://github.com/decred/dcrdex/blob/master/docs/images/zip-0317.png func TransparentTxFeesZIP317(txInSize, txOutSize uint64) uint64 { return txFeesZIP317(txInSize, txOutSize, nil) } diff --git a/dex/networks/zec/tx.go b/dex/networks/zec/tx.go index 81b00e1546..adca55b8d0 100644 --- a/dex/networks/zec/tx.go +++ b/dex/networks/zec/tx.go @@ -800,6 +800,7 @@ func (tx *Tx) SerializeSize() uint64 { return sz } +// TxFeesZIP317 calculates the tx fees according to ZIP-0317. func (tx *Tx) TxFeesZIP317() uint64 { txInsSize := uint64(wire.VarIntSerializeSize(uint64(len(tx.TxIn)))) for _, txIn := range tx.TxIn { diff --git a/docs/images/zip-0317.png b/docs/images/zip-0317.png deleted file mode 100644 index f4ba1cdc11..0000000000 Binary files a/docs/images/zip-0317.png and /dev/null differ diff --git a/docs/images/zip-0317.tex b/docs/images/zip-0317.tex deleted file mode 100644 index 84d1e5fa27..0000000000 --- a/docs/images/zip-0317.tex +++ /dev/null @@ -1,20 +0,0 @@ -\documentclass[]{article} -\begin{document} - -\[ -\begin{array}{rcl} - marginal\_fee &=& 5000 \\ - grace\_actions &=& 2 \\ - p2pkh\_standard\_input\_size &=& 150 \\ - p2pkh\_standard\_output\_size &=& 34 \\ - n\_transparent &=& \mathsf{max}\big(\mathsf{ceiling}\big(\frac{tx\_in\_total\_size}{p2pkh\_standard\_input\_size}\big), - \mathsf{ceiling}\big(\frac{tx\_out\_total\_size}{p2pkh\_standard\_output\_size}\big)\big) \\ - logical\_actions &=& n\_transparent \;+ \\ - & & 2 \cdot nJoinSplit \;+ \\ - & & \mathsf{max}(nSpendsSapling, nOutputsSapling) \;+ \\ - & & nActionsOrchard \\ - conventional\_fee &=& marginal\_fee \cdot \mathsf{max}(grace\_actions, logical\_actions) -\end{array} -\] - -\end{document} diff --git a/server/asset/btc/tx.go b/server/asset/btc/tx.go index b61fda61ce..707626aa61 100644 --- a/server/asset/btc/tx.go +++ b/server/asset/btc/tx.go @@ -26,8 +26,8 @@ type Tx struct { // Used to conditionally skip block lookups on mempool transactions during // calls to Confirmations. lastLookup *chainhash.Hash - // fees is the fees paid in the tx. fees is used Zcash. It is exposed by the - // (*TXIO).Fees which is not part of the asset.Coin interface. + // fees is the fees paid in the tx. fees is used by Zcash. It is exposed by + // the (*TXIO).Fees which is not part of the asset.Coin interface. fees uint64 // The calculated transaction fee rate, in satoshis/vbyte feeRate uint64 diff --git a/server/asset/zec/zec.go b/server/asset/zec/zec.go index b2a6a206e3..4519ae2d83 100644 --- a/server/asset/zec/zec.go +++ b/server/asset/zec/zec.go @@ -184,6 +184,8 @@ func (be *ZECBackend) ValidateFeeRate(contract *asset.Contract, reqFeeRate uint6 return fees >= zecTx.TxFeesZIP317() } +var _ asset.OrderEstimator = (*ZECBackend)(nil) + // CalcOrderFunds is the ZIP-0317 compliant version of calc.RequiredOrderFunds. // Satisfies the asset.OrderEstimator interface. func (be *ZECBackend) CalcOrderFunds(swapVal, inputCount, inputsSize, maxSwaps uint64) uint64 {