Skip to content

Commit

Permalink
implement zip 317
Browse files Browse the repository at this point in the history
  • Loading branch information
buck54321 committed May 3, 2023
1 parent b8e3a56 commit eb2af19
Show file tree
Hide file tree
Showing 18 changed files with 468 additions and 91 deletions.
201 changes: 143 additions & 58 deletions client/asset/btc/btc.go

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions client/asset/btc/btc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -669,9 +669,8 @@ func tNewWallet(segwit bool, walletType string) (*intermediaryWallet, *testData,
}
w.node = spvw
wallet = &intermediaryWallet{
baseWallet: w,
txFeeEstimator: spvw,
tipRedeemer: spvw,
baseWallet: w,
tipRedeemer: spvw,
}
}
}
Expand Down
25 changes: 16 additions & 9 deletions client/asset/btc/coin_selection.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import (
"sort"
"time"

"decred.org/dcrdex/dex/calc"
dexbtc "decred.org/dcrdex/dex/networks/btc"
)

func (btc *baseWallet) sendEnough(amt, feeRate uint64, subtract bool, baseTxSize uint64, segwit, reportChange bool) func(inputCount, inputSize, sum uint64) (bool, uint64) {
return sendEnough(amt, feeRate, subtract, baseTxSize, segwit, reportChange, btc.isDust, btc.txFees)
}

// sendEnough generates a function that can be used as the enough argument to
// the fund method when creating transactions to send funds. If fees are to be
// subtracted from the inputs, set subtract so that the required amount excludes
Expand All @@ -20,9 +23,9 @@ import (
// enough func will return a non-zero excess value. Otherwise, the enough func
// will always return 0, leaving only unselected UTXOs to cover any required
// reserves.
func sendEnough(amt, feeRate uint64, subtract bool, baseTxSize uint64, segwit, reportChange bool) func(inputSize, sum uint64) (bool, uint64) {
return func(inputSize, sum uint64) (bool, uint64) {
txFee := (baseTxSize + inputSize) * feeRate
func sendEnough(amt, feeRate uint64, subtract bool, baseTxSize uint64, segwit, reportChange bool, isDust DustRater, sendFees TxFeesCalculator) func(inputCount, inputSize, sum uint64) (bool, uint64) {
return func(inputCount, inputSize, sum uint64) (bool, uint64) {
txFee := sendFees(baseTxSize, inputCount, inputSize, feeRate)
req := amt
if !subtract { // add the fee to required
req += txFee
Expand All @@ -31,25 +34,29 @@ func sendEnough(amt, feeRate uint64, subtract bool, baseTxSize uint64, segwit, r
return false, 0
}
excess := sum - req
if !reportChange || dexbtc.IsDustVal(dexbtc.P2PKHOutputSize, excess, feeRate, segwit) {
if !reportChange || isDust(dexbtc.P2PKHOutputSize, excess, feeRate, segwit) {
excess = 0
}
return true, excess
}
}

func (btc *baseWallet) orderEnough(val, lots, feeRate, initTxSizeBase, initTxSize uint64, segwit, reportChange bool) func(inputCount, inputSize, sum uint64) (bool, uint64) {
return orderEnough(val, lots, feeRate, initTxSizeBase, initTxSize, segwit, reportChange, btc.orderReq, btc.isDust)
}

// orderEnough generates a function that can be used as the enough argument to
// the fund method. If change from a split transaction will be created AND
// immediately available, set reportChange to indicate this and the returned
// enough func will return a non-zero excess value reflecting this potential
// spit tx change. Otherwise, the enough func will always return 0, leaving
// only unselected UTXOs to cover any required reserves.
func orderEnough(val, lots, feeRate, initTxSizeBase, initTxSize uint64, segwit, reportChange bool) func(inputsSize, sum uint64) (bool, uint64) {
return func(inputsSize, sum uint64) (bool, uint64) {
reqFunds := calc.RequiredOrderFundsAlt(val, inputsSize, lots, initTxSizeBase, initTxSize, feeRate)
func orderEnough(val, lots, feeRate, initTxSizeBase, initTxSize uint64, segwit, reportChange bool, orderReq OrderEstimator, isDust DustRater) func(inputCount, inputsSize, sum uint64) (bool, uint64) {
return func(inputCount, inputsSize, sum uint64) (bool, uint64) {
reqFunds := orderReq(val, inputCount, inputsSize, lots, initTxSizeBase, initTxSize, feeRate)
if sum >= reqFunds {
excess := sum - reqFunds
if !reportChange || dexbtc.IsDustVal(dexbtc.P2PKHOutputSize, excess, feeRate, segwit) {
if !reportChange || isDust(dexbtc.P2PKHOutputSize, excess, feeRate, segwit) {
excess = 0
}
return true, excess
Expand Down
2 changes: 1 addition & 1 deletion client/asset/btc/rpcclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ func (wc *rpcClient) locked() bool {
return time.Unix(*walletInfo.UnlockedUntil, 0).Before(time.Now())
}

// sendTxFeeEstimator returns the fee required to send tx using the provided
// estimateSendTxFee returns the fee required to send tx using the provided
// feeRate.
func (wc *rpcClient) estimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract bool) (txfee uint64, err error) {
txBytes, err := wc.serializeTx(tx)
Expand Down
37 changes: 37 additions & 0 deletions client/asset/btc/simnet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import (
"decred.org/dcrdex/dex/config"
dexbtc "decred.org/dcrdex/dex/networks/btc"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)

Expand Down Expand Up @@ -61,6 +64,7 @@ func tBackend(t *testing.T, name string, blkFunc func(string, error)) (*Exchange
}
// settings["account"] = "default"
walletCfg := &asset.WalletConfig{
Type: walletTypeRPC,
Settings: settings,
TipChange: func(err error) {
blkFunc(name, err)
Expand Down Expand Up @@ -236,3 +240,36 @@ func TestMakeBondTx(t *testing.T) {
}
t.Logf("refundCoin: %v\n", refundCoin)
}

func TestSendEstimation(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)

addr, _ := btcutil.DecodeAddress("bcrt1qs6d2lpkcfccus6q7c0dvjnlpf5g45gf7yak6mm", &chaincfg.RegressionNetParams)
pkScript, _ := txscript.PayToAddrScript(addr)
tx := wire.NewMsgTx(wire.TxVersion)
tx.AddTxOut(wire.NewTxOut(10e8, pkScript))

// 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)
if err != nil {
t.Fatalf("Error estimating with utxos: %v", err)
}
}
fmt.Println("Time to pick utxos ourselves:", time.Since(tStart))
node := w.node.(*rpcClient)
tStart = time.Now()
for i := 0; i < numCycles; i++ {
_, err := node.estimateSendTxFee(tx, 20, false)
if err != nil {
t.Fatalf("Error estimating with utxos: %v", err)
}
}
fmt.Println("Time to use fundrawtransaction:", time.Since(tStart))
}
27 changes: 17 additions & 10 deletions client/asset/btc/spv_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -748,36 +748,43 @@ func (w *spvWallet) Lock() error {
return nil
}

// DRAFT NOTE: Move estimateSendTxFee out of spv_wrapper before merge. Leaving
// for reviewability.

// estimateSendTxFee callers should provide at least one output value.
func (w *spvWallet) estimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract bool) (fee uint64, err error) {
func (btc *baseWallet) estimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract bool) (fee uint64, err error) {
minTxSize := uint64(tx.SerializeSize())
var sendAmount uint64
for _, txOut := range tx.TxOut {
sendAmount += uint64(txOut.Value)
}

unspents, err := w.listUnspent()
unspents, err := btc.node.listUnspent()
if err != nil {
return 0, fmt.Errorf("error listing unspent outputs: %w", err)
}

utxos, _, _, err := convertUnspent(0, unspents, w.chainParams)
utxos, _, _, err := convertUnspent(0, unspents, btc.chainParams)
if err != nil {
return 0, fmt.Errorf("error converting unspent outputs: %w", err)
}

enough := sendEnough(sendAmount, feeRate, subtract, minTxSize, true, false)
sum, _, inputsSize, _, _, _, _, err := tryFund(utxos, enough)
enough := btc.sendEnough(sendAmount, feeRate, subtract, minTxSize, true, false)
sum, _, inputsSize, coins, _, _, _, err := tryFund(utxos, enough)
if err != nil {
return 0, err
}

txSize := minTxSize + inputsSize
estFee := feeRate * txSize
estFee := btc.txFees(minTxSize, uint64(len(coins)), inputsSize, feeRate)
remaining := sum - sendAmount

var opSize uint64 = dexbtc.P2PKHOutputSize
if btc.segwit {
opSize = dexbtc.P2WPKHOutputSize
}

// Check if there will be a change output if there is enough remaining.
estFeeWithChange := (txSize + dexbtc.P2WPKHOutputSize) * feeRate
estFeeWithChange := btc.txFees(minTxSize+opSize, uint64(len(coins)), inputsSize, feeRate)
var changeValue uint64
if remaining > estFeeWithChange {
changeValue = remaining - estFeeWithChange
Expand All @@ -789,7 +796,7 @@ func (w *spvWallet) estimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract b
}

var finalFee uint64
if dexbtc.IsDustVal(dexbtc.P2WPKHOutputSize, changeValue, feeRate, true) {
if btc.isDust(opSize, changeValue, feeRate, true) {
// remaining cannot cover a non-dust change and the fee for the change.
finalFee = estFee + remaining
} else {
Expand All @@ -800,7 +807,7 @@ func (w *spvWallet) estimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract b
if subtract {
sendAmount -= finalFee
}
if dexbtc.IsDustVal(minTxSize, sendAmount, feeRate, true) {
if btc.isDust(minTxSize, sendAmount, feeRate, true) {
return 0, errors.New("output value is dust")
}

Expand Down
3 changes: 2 additions & 1 deletion client/asset/zec/shielded_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ const (

// z_sendmany "fromaddress" [{"address":... ,"amount":...},...] ( minconf ) ( fee ) ( privacyPolicy )
func zSendMany(c rpcCaller, fromAddress string, recips []*zSendManyRecipient, priv privacyPolicy) (operationID string, err error) {
const minConf, fee = 1, 0.00001
const minConf = 1
var fee *uint64 // Only makes sense with >= 5.5.0
return operationID, c.CallRPC(methodZSendMany, []interface{}{fromAddress, recips, minConf, fee, priv}, &operationID)
}

Expand Down
87 changes: 85 additions & 2 deletions client/asset/zec/zec.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const (
// structure.
defaultFee = 10
defaultFeeRateLimit = 1000
minNetworkVersion = 5040250 // v5.4.2
minNetworkVersion = 5050050 // v5.5.0
walletTypeRPC = "zcashdRPC"

transparentAcctNumber = 0
Expand Down Expand Up @@ -210,7 +210,17 @@ func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, net dex.Network) (ass
return dexzec.EncodeAddress(addr, addrParams)
},
TxSizeCalculator: dexzec.CalcTxSize,
NonSegwitSigner: signTx,
DustRater: func(outputSize, value, minRelayTxFee uint64, segwit bool) bool {
return isDust(value, outputSize)
},
OrderEstimator: func(swapVal, inputCount, inputsSize, maxSwaps, swapSizeBase, _, _ uint64) uint64 {
return dexzec.RequiredOrderFunds(swapVal, inputCount, inputsSize, maxSwaps)
},
TxFeesCalculator: func(baseTxSize, inputCount, inputsSize, _ uint64) uint64 {
return txFees(baseTxSize, inputCount, inputsSize)
},
SplitFeeCalculator: splitTxFees,
NonSegwitSigner: signTx,
TxDeserializer: func(b []byte) (*wire.MsgTx, error) {
zecTx, err := dexzec.DeserializeTx(b)
if err != nil {
Expand Down Expand Up @@ -259,6 +269,44 @@ type zecWallet struct {
lastAddress atomic.Value // "string"
}

func (w *zecWallet) FeeRate(context.Context) (uint64, error) {
return 10, nil
}

func (w *zecWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) {
est, err := w.ExchangeWalletNoAuth.PreSwap(req)
if err != nil {
return nil, err
}
// We need to strip out the fee bump option.
const swapFeeBumpKey = "swapfeebump"
opts := make([]*asset.OrderOption, 0, len(est.Options))
for _, opt := range est.Options {
if opt.Key == swapFeeBumpKey {
continue
}
opts = append(opts, opt)
}
est.Options = opts
return est, nil
}

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
return &asset.PreRedeem{
Estimate: &asset.RedeemEstimate{
RealisticWorstCase: worstCaseFees,
RealisticBestCase: bestCaseFees,
},
Options: []*asset.OrderOption{},
}, nil
}

var _ asset.ShieldedWallet = (*zecWallet)(nil)

func transparentAddress(c rpcCaller, addrParams *dexzec.AddressParams, btcParams *chaincfg.Params) (btcutil.Address, error) {
Expand Down Expand Up @@ -600,3 +648,38 @@ func signTx(btcTx *wire.MsgTx, idx int, pkScript []byte, hashType txscript.SigHa

return append(ecdsa.Sign(key, sigHash[:]).Serialize(), byte(hashType)), nil
}

// isDust returns true if the output will be rejected as dust.
func isDust(val, outputSize uint64) bool {
// See https://github.com/zcash/zcash/blob/5066efbb98bc2af5eed201212d27c77993950cee/src/primitives/transaction.h#L630
// https://github.com/zcash/zcash/blob/5066efbb98bc2af5eed201212d27c77993950cee/src/primitives/transaction.cpp#L127
// Also see informative comments hinting towards future changes at
// https://github.com/zcash/zcash/blob/master/src/policy/policy.h
sz := outputSize + 148 // 148 accounts for an input on spending tx
const oneThirdDustThresholdRate = 100 // zats / kB
nFee := oneThirdDustThresholdRate * sz / 1000 // This is different from BTC
if nFee == 0 {
nFee = oneThirdDustThresholdRate
}
return val < 3*nFee
}

var emptyTxSize = dexzec.CalcTxSize(new(wire.MsgTx))

// txFees is the tx fees for a basic single-output send with change.
func txFees(baseTxSize, inputCount, inputsSize uint64) uint64 {
txInSize := inputsSize + uint64(wire.VarIntSerializeSize(inputCount))
outputsSize := baseTxSize - emptyTxSize
txOutSize := outputsSize + 1 // Assumes < 253 outputs
return dexzec.TransparentTxFeesZIP317(txInSize, txOutSize)
}

func splitTxFees(inputCount, inputsSize, _ uint64, extraOutput, _ bool) (swapInputSize, baggage uint64) {
txInsSize := inputsSize + uint64(wire.VarIntSerializeSize(inputCount))
var numOutputs uint64 = 2
if extraOutput {
numOutputs = 3
}
txOutsSize := dexbtc.P2PKHOutputSize*numOutputs + uint64(wire.VarIntSerializeSize(numOutputs))
return dexbtc.RedeemP2PKHInputSize, dexzec.TransparentTxFeesZIP317(txInsSize, txOutsSize)
}
59 changes: 59 additions & 0 deletions dex/networks/zec/script.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org

package zec

import (
"math"

dexbtc "decred.org/dcrdex/dex/networks/btc"
"github.com/btcsuite/btcd/wire"
)

// 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)
}

// txFeexZIP317 calculates fees for a transaction. If the tx is
// fully-transparent, the shieldedTx argument can be nil. The caller must sum up
// the txin and txout, which is the entire serialization size associated with
// the respective field, including the size of the count varint.
func txFeesZIP317(transparentTxInsSize, transparentTxOutsSize uint64, shieldedTx *Tx) uint64 {
const (
marginalFee = 5000
graceActions = 2
pkhStandardInputSize = 150
pkhStandardOutputSize = 34
)

nIn := math.Ceil(float64(transparentTxInsSize) / pkhStandardInputSize)
nOut := math.Ceil(float64(transparentTxOutsSize) / pkhStandardOutputSize)

logicalActions := uint64(math.Max(nIn, nOut))
if shieldedTx != nil {
nSapling := uint64(math.Max(float64(shieldedTx.NSpendsSapling), float64(shieldedTx.NOutputsSapling)))
logicalActions += 2*shieldedTx.NJoinSplit + nSapling + shieldedTx.NActionsOrchard
}

return marginalFee * uint64(math.Max(graceActions, float64(logicalActions)))
}

// RequiredOrderFunds is the ZIP-0317 compliant version of
// calc.RequiredOrderFunds.
func RequiredOrderFunds(swapVal, inputCount, inputsSize, maxSwaps uint64) uint64 {
// One p2sh output for the contract, 1 change output.
const txOutsSize = dexbtc.P2PKHOutputSize + dexbtc.P2SHOutputSize + 1 /* wire.VarIntSerializeSize(2) */
txInsSize := inputsSize + uint64(wire.VarIntSerializeSize(inputCount))
firstTxFees := TransparentTxFeesZIP317(txInsSize, txOutsSize)
if maxSwaps == 1 {
return swapVal + firstTxFees
}

otherTxsFees := TransparentTxFeesZIP317(dexbtc.RedeemP2PKHInputSize+1, txOutsSize)
fees := firstTxFees + (maxSwaps-1)*otherTxsFees
return swapVal + fees
}
Loading

0 comments on commit eb2af19

Please sign in to comment.