decred.org/dcrdex@v1.0.5/server/asset/zec/zec.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package zec 5 6 import ( 7 "context" 8 "fmt" 9 "math" 10 11 "decred.org/dcrdex/dex" 12 dexbtc "decred.org/dcrdex/dex/networks/btc" 13 dexzec "decred.org/dcrdex/dex/networks/zec" 14 "decred.org/dcrdex/server/asset" 15 "decred.org/dcrdex/server/asset/btc" 16 "github.com/btcsuite/btcd/btcutil" 17 "github.com/btcsuite/btcd/chaincfg" 18 "github.com/btcsuite/btcd/chaincfg/chainhash" 19 "github.com/btcsuite/btcd/wire" 20 ) 21 22 // Driver implements asset.Driver. 23 type Driver struct{} 24 25 // Setup creates the Zcash backend. Start the backend with its Run method. 26 func (d *Driver) Setup(cfg *asset.BackendConfig) (asset.Backend, error) { 27 return NewBackend(cfg) 28 } 29 30 // DecodeCoinID creates a human-readable representation of a coin ID for 31 // Zcash. 32 func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { 33 // Zcash and Bitcoin have the same tx hash and output format. 34 return (&btc.Driver{}).DecodeCoinID(coinID) 35 } 36 37 // Version returns the Backend implementation's version number. 38 func (d *Driver) Version() uint32 { 39 return version 40 } 41 42 // UnitInfo returns the dex.UnitInfo for the asset. 43 func (d *Driver) UnitInfo() dex.UnitInfo { 44 return dexzec.UnitInfo 45 } 46 47 // Name is the asset's name. 48 func (d *Driver) Name() string { 49 return "Zcash" 50 } 51 52 // MinBondSize calculates the minimum bond size for a given fee rate that avoids 53 // dust outputs on the bond and refund txs, assuming the maxFeeRate doesn't 54 // change. 55 func (d *Driver) MinBondSize(maxFeeRate uint64) uint64 { 56 var inputsSize uint64 = dexbtc.TxInOverhead + dexbtc.RedeemBondSigScriptSize + 1 57 var outputsSize uint64 = dexbtc.P2PKHOutputSize + 1 58 refundBondTxFees := dexzec.TxFeesZIP317(inputsSize, outputsSize, 0, 0, 0, 0) 59 return dexzec.MinHTLCSize(refundBondTxFees) 60 } 61 62 // MinLotSize calculates the minimum bond size for a given fee rate that avoids 63 // dust outputs on the swap and refund txs, assuming the maxFeeRate doesn't 64 // change. 65 func (d *Driver) MinLotSize(maxFeeRate uint64) uint64 { 66 return dexzec.MinLotSize(maxFeeRate) 67 } 68 69 func init() { 70 asset.Register(BipID, &Driver{}) 71 } 72 73 const ( 74 version = 0 75 BipID = 133 76 assetName = "zec" 77 feeConfs = 10 // Block time is 75 seconds 78 ) 79 80 // NewBackend generates the network parameters and creates a zec backend as a 81 // btc clone using an asset/btc helper function. 82 func NewBackend(cfg *asset.BackendConfig) (asset.Backend, error) { 83 var btcParams *chaincfg.Params 84 var addrParams *dexzec.AddressParams 85 switch cfg.Net { 86 case dex.Mainnet: 87 btcParams = dexzec.MainNetParams 88 addrParams = dexzec.MainNetAddressParams 89 case dex.Testnet: 90 btcParams = dexzec.TestNet4Params 91 addrParams = dexzec.TestNet4AddressParams 92 case dex.Regtest: 93 btcParams = dexzec.RegressionNetParams 94 addrParams = dexzec.RegressionNetAddressParams 95 default: 96 return nil, fmt.Errorf("unknown network ID %v", cfg.Net) 97 } 98 99 // Designate the clone ports. These will be overwritten by any explicit 100 // settings in the configuration file. 101 ports := dexbtc.NetPorts{ 102 Mainnet: "8232", 103 Testnet: "18232", 104 Simnet: "18232", 105 } 106 107 configPath := cfg.ConfigPath 108 if configPath == "" { 109 configPath = dexbtc.SystemConfigPath("zcash") 110 } 111 112 be, err := btc.NewBTCClone(&btc.BackendCloneConfig{ 113 Name: assetName, 114 Segwit: false, 115 ConfigPath: configPath, 116 Logger: cfg.Logger, 117 Net: cfg.Net, 118 ChainParams: btcParams, 119 Ports: ports, 120 AddressDecoder: func(addr string, net *chaincfg.Params) (btcutil.Address, error) { 121 return dexzec.DecodeAddress(addr, addrParams, btcParams) 122 }, 123 TxDeserializer: func(b []byte) (*wire.MsgTx, error) { 124 zecTx, err := dexzec.DeserializeTx(b) 125 if err != nil { 126 return nil, err 127 } 128 return zecTx.MsgTx, nil 129 }, 130 TxHasher: func(tx *wire.MsgTx) *chainhash.Hash { 131 h := zecTx(tx).TxHash() 132 return &h 133 }, 134 BlockDeserializer: func(b []byte) (*wire.MsgBlock, error) { 135 zecBlock, err := dexzec.DeserializeBlock(b) 136 if err != nil { 137 return nil, err 138 } 139 return &zecBlock.MsgBlock, nil 140 }, 141 DumbFeeEstimates: true, 142 FeeConfs: feeConfs, 143 ManualMedianFee: true, 144 BlockFeeTransactions: blockFeeTransactions, 145 NumericGetRawRPC: true, 146 ShieldedIO: shieldedIO, 147 RelayAddr: cfg.RelayAddr, 148 }) 149 if err != nil { 150 return nil, err 151 } 152 153 return &ZECBackend{ 154 Backend: be, 155 log: cfg.Logger, 156 addrParams: addrParams, 157 btcParams: btcParams, 158 }, nil 159 } 160 161 // ZECBackend embeds *btc.Backend and re-implements the Contract method to deal 162 // with Zcash address translation. 163 type ZECBackend struct { 164 *btc.Backend 165 log dex.Logger 166 btcParams *chaincfg.Params 167 addrParams *dexzec.AddressParams 168 } 169 170 // Contract returns the output from embedded Backend's Contract method, but 171 // with the SwapAddress field converted to Zcash encoding. 172 // TODO: Drop this in favor of an AddressEncoder field in the 173 // BackendCloneConfig. 174 func (be *ZECBackend) Contract(coinID []byte, redeemScript []byte) (*asset.Contract, error) { // Contract.SwapAddress 175 contract, err := be.Backend.Contract(coinID, redeemScript) 176 if err != nil { 177 return nil, err 178 } 179 contract.SwapAddress, err = dexzec.RecodeAddress(contract.SwapAddress, be.addrParams, be.btcParams) 180 if err != nil { 181 return nil, err 182 } 183 return contract, nil 184 } 185 186 // For Zcash, return a constant fee rate of 10 zats / byte. We just need to 187 // guarantee the tx get over the legacy 0.00001 standard tx fee. 188 func (be *ZECBackend) FeeRate(context.Context) (uint64, error) { 189 return dexzec.LegacyFeeRate, nil 190 } 191 192 func (*ZECBackend) ValidateOrderFunding(swapVal, valSum, inputCount, inputsSize, maxSwaps uint64, _ *dex.Asset) bool { 193 reqVal := dexzec.RequiredOrderFunds(swapVal, inputCount, inputsSize, maxSwaps) 194 return valSum >= reqVal 195 } 196 197 func (be *ZECBackend) ValidateFeeRate(ci asset.Coin, _ uint64) bool { 198 c, is := ci.(interface { 199 InputsValue() uint64 200 RawTx() []byte 201 }) 202 if !is { 203 be.log.Error("ValidateFeeRate contract does not implement TXIO methods") 204 return false 205 } 206 tx, err := dexzec.DeserializeTx(c.RawTx()) 207 if err != nil { 208 be.log.Errorf("error deserializing tx for fee validation: %v", err) 209 return false 210 } 211 212 fees, err := newFeeTx(tx).Fees(c.InputsValue()) 213 if err != nil { 214 be.log.Errorf("error calculating tx fees: %v", err) 215 return false 216 } 217 218 return fees >= tx.RequiredTxFeesZIP317() 219 } 220 221 func blockFeeTransactions(rc *btc.RPCClient, blockHash *chainhash.Hash) (feeTxs []btc.FeeTx, prevBlock chainhash.Hash, err error) { 222 blockB, err := rc.GetRawBlock(blockHash) 223 if err != nil { 224 return nil, chainhash.Hash{}, err 225 } 226 227 blk, err := dexzec.DeserializeBlock(blockB) 228 if err != nil { 229 return nil, chainhash.Hash{}, err 230 } 231 232 if len(blk.Transactions) == 0 { 233 return nil, chainhash.Hash{}, fmt.Errorf("block %s has no transactions", blockHash) 234 } 235 236 feeTxs = make([]btc.FeeTx, 0, len(blk.Transactions)-1) 237 for _, tx := range blk.Transactions[1:] { // skip coinbase 238 feeTx := newFeeTx(tx) 239 feeTxs = append(feeTxs, feeTx) 240 } 241 242 return feeTxs, blk.Header.PrevBlock, nil 243 } 244 245 // feeTx implements FeeTx for manual median-fee calculations. 246 type feeTx struct { 247 size uint64 248 prevOuts []wire.OutPoint 249 shieldedIn uint64 250 transparentOut uint64 251 shieldedOut uint64 252 } 253 254 var _ btc.FeeTx = (*feeTx)(nil) 255 256 func newFeeTx(zecTx *dexzec.Tx) *feeTx { 257 var transparentOut uint64 258 for _, out := range zecTx.TxOut { 259 transparentOut += uint64(out.Value) 260 } 261 prevOuts := make([]wire.OutPoint, 0, len(zecTx.TxIn)) 262 for _, in := range zecTx.TxIn { 263 prevOuts = append(prevOuts, in.PreviousOutPoint) 264 } 265 var shieldedIn, shieldedOut uint64 266 for _, js := range zecTx.VJoinSplit { 267 shieldedIn += js.New 268 shieldedOut += js.Old 269 } 270 if zecTx.ValueBalanceSapling > 0 { 271 shieldedIn += uint64(zecTx.ValueBalanceSapling) 272 } else if zecTx.ValueBalanceSapling < 0 { 273 shieldedOut += uint64(-1 * zecTx.ValueBalanceSapling) 274 } 275 if zecTx.ValueBalanceOrchard > 0 { 276 shieldedIn += uint64(zecTx.ValueBalanceOrchard) 277 } else if zecTx.ValueBalanceOrchard < 0 { 278 shieldedOut += uint64(-1 * zecTx.ValueBalanceOrchard) 279 } 280 281 return &feeTx{ 282 size: zecTx.SerializeSize(), 283 transparentOut: transparentOut, 284 shieldedOut: shieldedOut, 285 shieldedIn: shieldedIn, 286 prevOuts: prevOuts, 287 } 288 } 289 290 func (tx *feeTx) PrevOuts() []wire.OutPoint { 291 return tx.prevOuts 292 } 293 294 func (tx *feeTx) FeeRate(prevOuts map[chainhash.Hash]map[int]int64) (uint64, error) { 295 var transparentIn uint64 296 for _, op := range tx.prevOuts { 297 outs, found := prevOuts[op.Hash] 298 if !found { 299 return 0, fmt.Errorf("previous outpoint tx not found for %+v", op) 300 } 301 prevOutValue, found := outs[int(op.Index)] 302 if !found { 303 return 0, fmt.Errorf("previous outpoint vout not found for %+v", op) 304 } 305 transparentIn += uint64(prevOutValue) 306 } 307 fees, err := tx.Fees(transparentIn) 308 if err != nil { 309 return 0, err 310 } 311 return uint64(math.Round(float64(fees) / float64(tx.size))), nil 312 } 313 314 func (tx *feeTx) Fees(transparentIn uint64) (uint64, error) { 315 in := tx.shieldedIn + transparentIn 316 out := tx.shieldedOut + tx.transparentOut 317 if out > in { 318 return 0, fmt.Errorf("out > in. %d > %d", out, in) 319 } 320 return in - out, nil 321 } 322 323 func shieldedIO(tx *btc.VerboseTxExtended) (in, out uint64, err error) { 324 zecTx, err := dexzec.DeserializeTx(tx.Raw) 325 if err != nil { 326 return 0, 0, fmt.Errorf("DeserializeTx error: %w", err) 327 } 328 feeTx := newFeeTx(zecTx) 329 return feeTx.shieldedIn, feeTx.shieldedOut, nil 330 } 331 332 func zecTx(tx *wire.MsgTx) *dexzec.Tx { 333 return dexzec.NewTxFromMsgTx(tx, dexzec.MaxExpiryHeight) 334 }