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

zec: implement zip 317 #2338

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
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
243 changes: 173 additions & 70 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 @@ -855,7 +855,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
47 changes: 47 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,46 @@ func TestMakeBondTx(t *testing.T) {
}
t.Logf("refundCoin: %v\n", refundCoin)
}

// 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)
})
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)
tx := wire.NewMsgTx(wire.TxVersion)
tx.AddTxOut(wire.NewTxOut(10e8, pkScript))

// Use alpha, since there are many utxos.
w := rig.alpha()
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)
}
}
manualTime = 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)
}
}
rpcTime = 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 @@ -32,7 +32,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 @@ -207,7 +207,19 @@ 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: 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 {
Expand Down Expand Up @@ -263,6 +275,40 @@ func (w *zecWallet) FeeRate() uint64 {
return dexzec.LegacyFeeRate
}

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
// 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,
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 @@ -587,3 +633,40 @@ 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 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
txOutSize := outputsSize + 1 // Assumes < 253 outputs
return dexzec.TransparentTxFeesZIP317(txInSize, txOutSize)
}

// 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 {
numOutputs = 3
}
txOutsSize := dexbtc.P2PKHOutputSize*numOutputs + uint64(wire.VarIntSerializeSize(numOutputs))
return dexbtc.RedeemP2PKHInputSize, dexzec.TransparentTxFeesZIP317(txInsSize, txOutsSize)
}
Loading