diff --git a/decred/decred/dcr/account.py b/decred/decred/dcr/account.py index 58756251..19cca01b 100644 --- a/decred/decred/dcr/account.py +++ b/decred/decred/dcr/account.py @@ -89,37 +89,43 @@ def __init__( spendLimit, poolAddress, votingAddress, - ticketFee, poolFees, count, txFee, + ticketFee=0, ): - # minConf is just a placeholder for now. Account minconf is 0 until - # I add the ability to change it. + """ + TicketRequest constructor. + + Args: + minConf (int): minConf is just a placeholder for now. Account + minconf is 0 until I add the ability to change it. + expiry (int): expiry can be set to some reasonable block height. + This may be important when approaching the end of a ticket + window. + spendLimit (int): Price is calculated purely from the ticket count, + price, and fees, but cannot go over spendLimit. + poolAddress (str): The VSP fee payment address. + votingAddress (str): The P2SH voting address based on the 1-of-2. + multi-sig script you share with the VSP. + poolFees (int): poolFees are set by the VSP. If you don't set these + correctly, the VSP may not vote for you. + count (int): How many tickets to buy. + txFee (int): txFee is the transaction fee rate to pay the miner for + the split transaction required to fund the ticket. + ticketFee (int): Optional. Default is the network's default relay + fee. ticketFee is the transaction fee rate to pay the miner for + the ticket. + """ self.minConf = minConf - # expiry can be set to some reasonable block height. This may be - # important when approaching the end of a ticket window. self.expiry = expiry - # Price is calculated purely from the ticket count, price, and fees, but - # cannot go over spendLimit. self.spendLimit = spendLimit - # The VSP fee payment address. self.poolAddress = poolAddress - # The P2SH voting address based on the 1-of-2 multi-sig script you share - # with the VSP. self.votingAddress = votingAddress - # ticketFee is the transaction fee rate to pay the miner for the ticket. - # Set to zero to use wallet's network default fee rate. - self.ticketFee = ticketFee - # poolFees are set by the VSP. If you don't set these correctly, the - # VSP may not vote for you. self.poolFees = poolFees - # How many tickets to buy. self.count = count - # txFee is the transaction fee rate to pay the miner for the split - # transaction required to fund the ticket. - # Set to zero to use wallet's network default fee rate. self.txFee = txFee + self.ticketFee = ticketFee if ticketFee != 0 else DefaultRelayFeePerKb class TicketStats: @@ -1865,7 +1871,7 @@ def addressSignal(self, addr, txid): # signal the balance update self.signals.balance(self.calcBalance()) - def sendToAddress(self, value, address, feeRate=None): + def sendToAddress(self, value, address): """ Send the value to the address. @@ -1880,7 +1886,7 @@ def sendToAddress(self, value, address, feeRate=None): priv=self.privKeyForAddress, internal=self.nextInternalAddress, ) tx, spentUTXOs, newUTXOs = self.blockchain.sendToAddress( - value, address, keysource, self.getUTXOs, feeRate + value, address, keysource, self.getUTXOs, self.relayFee ) self.addMempoolTx(tx) self.spendUTXOs(spentUTXOs) @@ -1895,6 +1901,13 @@ def purchaseTickets(self, qty, price): Account uses the blockchain to do the heavy lifting, but must prepare the TicketRequest and KeySource and gather some other account- related information. + + Args: + qty (int): The number of tickets to buy. + price (int): The price per ticket in coins to pay. + + Returs: + MsgTx: The sent split transaction. """ keysource = KeySource( priv=self.privKeyForAddress, internal=self.nextInternalAddress, @@ -1907,10 +1920,9 @@ def purchaseTickets(self, qty, price): spendLimit=int(round(price * qty * 1.1 * 1e8)), # convert to atoms here poolAddress=pi.poolAddress, votingAddress=pi.ticketAddress, - ticketFee=0, # use network default poolFees=pi.poolFees, count=qty, - txFee=0, # use network default + txFee=self.relayFee, ) txs, spentUTXOs, newUTXOs = self.blockchain.purchaseTickets( keysource, self.getUTXOs, req @@ -1962,7 +1974,7 @@ def revokeTickets(self): priv=lambda _: self._votingKey, internal=lambda: "", ) - self.blockchain.revokeTicket(tx, keysource, redeemScript) + self.blockchain.revokeTicket(tx, keysource, redeemScript, self.relayFee) def sync(self): """ diff --git a/decred/decred/dcr/dcrdata.py b/decred/decred/dcr/dcrdata.py index a8cf41e0..ffa5f698 100644 --- a/decred/decred/dcr/dcrdata.py +++ b/decred/decred/dcr/dcrdata.py @@ -841,15 +841,6 @@ def updateTip(self): log.error("failed to retrieve tip from blockchain: %s" % formatTraceback(e)) raise DecredError("no tip data retrieved") - def relayFee(self): - """ - Return the current transaction fee. - - Returns: - int: Atoms per kB of encoded transaction. - """ - return txscript.DefaultRelayFeePerKb - def saveBlockHeader(self, header): """ Save the block header to the database. @@ -861,13 +852,25 @@ def saveBlockHeader(self, header): self.heightMap[header.height] = bHash self.headerDB[bHash] = header - def sendToAddress(self, value, address, keysource, utxosource, feeRate=None): + def checkFeeRate(self, relayFee): + """ + Check that the relay fee is lower than the allowed max of + txscript.HighFeeRate. + """ + if relayFee > txscript.HighFeeRate: + raise DecredError( + f"relay fee of {relayFee} is above the allowed max of {txscript.HighFeeRate}" + ) + + def sendToAddress( + self, value, address, keysource, utxosource, relayFee, allowHighFees=False + ): """ Send the amount in atoms to the specified address. Args: - value int: The amount to send, in atoms. - address str: The base-58 encoded address. + value (int): The amount to send, in atoms. + address (str): The base-58 encoded address. keysource func(str) -> PrivateKey: A function that returns the private key for an address. utxosource func(int, func(UTXO) -> bool) -> list(UTXO): A function @@ -876,11 +879,18 @@ def sendToAddress(self, value, address, keysource, utxosource, feeRate=None): amount. If the filtering function is provided, UTXOs for which the function return a falsey value will not be included in the returned UTXO list. - MsgTx: The newly created transaction on success, `False` on failure. + relayFee (int): Transaction fees in atoms per kb. + allowHighFees (bool): Optional. Default is False. Whether to allow + fees higher than txscript.HighFeeRate. + + Returns: + MsgTx: The newly created transaction. Raises an exception on error. """ + if not allowHighFees: + self.checkFeeRate(relayFee) self.updateTip() outputs = makeOutputs([(address, value)], self.netParams) - return self.sendOutputs(outputs, keysource, utxosource, feeRate) + return self.sendOutputs(outputs, keysource, utxosource, relayFee, allowHighFees) def broadcast(self, txHex): """ @@ -962,7 +972,9 @@ def confirmUTXO(self, utxo, block=None, tx=None): pass return False - def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): + def sendOutputs( + self, outputs, keysource, utxosource, relayFee, allowHighFees=False + ): """ Send the `TxOut`s to the address. @@ -982,12 +994,17 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): sufficient to complete the transaction. If the filtering function is provided, UTXOs for which the function return a falsey value will not be included in the returned UTXO list. + relayFee (int): Transaction fees in atoms per kb. + allowHighFees (bool): Optional. Default is False. Whether to allow + fees higher than txscript.HighFeeRate. Returns: newTx MsgTx: The sent transaction. utxos list(UTXO): The spent UTXOs. newUTXOs list(UTXO): Length 1 array containing the new change UTXO. """ + if not allowHighFees: + self.checkFeeRate(relayFee) total = 0 inputs = [] scripts = [] @@ -998,14 +1015,13 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): changeScriptVersion = txscript.DefaultScriptVersion changeScriptSize = txscript.P2PKHPkScriptSize - relayFeePerKb = feeRate * 1e3 if feeRate else self.relayFee() for (i, txout) in enumerate(outputs): - checkOutput(txout, relayFeePerKb) + checkOutput(txout, relayFee) signedSize = txscript.estimateSerializeSize( [txscript.RedeemP2PKHSigScriptSize], outputs, changeScriptSize ) - targetFee = txscript.calcMinRequiredTxRelayFee(relayFeePerKb, signedSize) + targetFee = txscript.calcMinRequiredTxRelayFee(relayFee, signedSize) targetAmount = sum(txo.value for txo in outputs) while True: @@ -1034,7 +1050,7 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): signedSize = txscript.estimateSerializeSize( scriptSizes, outputs, changeScriptSize ) - requiredFee = txscript.calcMinRequiredTxRelayFee(relayFeePerKb, signedSize) + requiredFee = txscript.calcMinRequiredTxRelayFee(relayFee, signedSize) remainingAmount = total - targetAmount if remainingAmount < requiredFee: targetFee = requiredFee @@ -1055,7 +1071,7 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): changeVout = -1 changeAmount = round(total - targetAmount - requiredFee) if changeAmount != 0 and not txscript.isDustAmount( - changeAmount, changeScriptSize, relayFeePerKb + changeAmount, changeScriptSize, relayFee ): if len(changeScript) > txscript.MaxScriptElementSize: raise DecredError( @@ -1110,7 +1126,7 @@ def sendOutputs(self, outputs, keysource, utxosource, feeRate=None): return newTx, utxos, newUTXOs - def purchaseTickets(self, keysource, utxosource, req): + def purchaseTickets(self, keysource, utxosource, req, allowHighFees=False): """ Based on dcrwallet (*Wallet).purchaseTickets. purchaseTickets indicates to the wallet that a ticket should be @@ -1120,11 +1136,13 @@ def purchaseTickets(self, keysource, utxosource, req): available. Args: - keysource account.KeySource: a source for private keys. + keysource (account.KeySource): a source for private keys. utxosource func(int, filterFunc) -> (list(UTXO), bool): a source for UTXOs. The filterFunc is an optional function to filter UTXOs, and is of the form func(UTXO) -> bool. - req account.TicketRequest: the ticket data. + req (account.TicketRequest): the ticket data. + allowHighFees (bool): Optional. Default is False. Whether to allow + fees higher than txscript.HighFeeRate. Returns: (splitTx, tickets) tuple: first element is the split transaction. @@ -1135,6 +1153,9 @@ def purchaseTickets(self, keysource, utxosource, req): addresses. """ + if not allowHighFees: + self.checkFeeRate(req.txFee) + self.checkFeeRate(req.ticketFee) self.updateTip() # account minConf is zero for regular outputs for now. Need to make that # adjustable. @@ -1205,10 +1226,6 @@ def purchaseTickets(self, keysource, utxosource, req): "unsupported voting address type %s" % votingAddress.__class__.__name__ ) - ticketFeeIncrement = req.ticketFee - if ticketFeeIncrement == 0: - ticketFeeIncrement = self.relayFee() - # Make sure that we have enough funds. Calculate different # ticket required amounts depending on whether or not a # pool output is needed. If the ticket fee increment is @@ -1232,7 +1249,7 @@ def purchaseTickets(self, keysource, utxosource, req): ] estSize = txscript.estimateSerializeSizeFromScriptSizes(inSizes, outSizes, 0) - ticketFee = txscript.calcMinRequiredTxRelayFee(ticketFeeIncrement, estSize) + ticketFee = txscript.calcMinRequiredTxRelayFee(req.ticketFee, estSize) neededPerTicket = ticketFee + ticketPrice # If we need to calculate the amount for a pool fee percentage, @@ -1270,14 +1287,9 @@ def purchaseTickets(self, keysource, utxosource, req): # User amount. splitOuts.append(msgtx.TxOut(value=userAmt, pkScript=splitPkScript,)) - txFeeIncrement = req.txFee - if txFeeIncrement == 0: - txFeeIncrement = self.relayFee() - # Send the split transaction. - # sendOutputs takes the fee rate in atoms/byte splitTx, splitSpent, internalOutputs = self.sendOutputs( - splitOuts, keysource, utxosource, int(txFeeIncrement / 1000) + splitOuts, keysource, utxosource, req.txFee, allowHighFees ) # Generate the tickets individually. @@ -1373,7 +1385,7 @@ def purchaseTickets(self, keysource, utxosource, req): ) return (splitTx, tickets), splitSpent, internalOutputs - def revokeTicket(self, tx, keysource, redeemScript): + def revokeTicket(self, tx, keysource, redeemScript, relayFee, allowHighFees=False): """ Revoke a ticket by signing the supplied redeem script and broadcasting the raw transaction. @@ -1384,12 +1396,17 @@ def revokeTicket(self, tx, keysource, redeemScript): the private key used for signing. redeemScript (byte-like): the 1-of-2 multisig script that delegates voting rights for the ticket. + relayFee (int): Transaction fees in atoms per kb. + allowHighFees (bool): Optional. Default is False. Whether to allow + fees higher than txscript.HighFeeRate. Returns: MsgTx: the signed revocation. """ + if not allowHighFees: + self.checkFeeRate(relayFee) - revocation = txscript.makeRevocation(tx, self.relayFee()) + revocation = txscript.makeRevocation(tx, relayFee) signedScript = txscript.signTxOutput( self.netParams, diff --git a/decred/decred/dcr/txscript.py b/decred/decred/dcr/txscript.py index 9c80bbbe..cab1b4be 100644 --- a/decred/decred/dcr/txscript.py +++ b/decred/decred/dcr/txscript.py @@ -154,7 +154,10 @@ # to maintain reference. # DefaultRelayFeePerKb is the default minimum relay fee policy for a mempool. -DefaultRelayFeePerKb = 1e4 +DefaultRelayFeePerKb = int(1e4) + +# HighFeeRate is the atoms/kb rate that is considered too high. +HighFeeRate = DefaultRelayFeePerKb * 1000 # MaxStandardTxSize is the maximum size allowed for transactions that are # considered standard and will therefore be relayed and considered for mining. @@ -2605,7 +2608,7 @@ def paysHighFees(totalInput, tx): # Impossible to determine return False - maxFee = calcMinRequiredTxRelayFee(1000 * DefaultRelayFeePerKb, tx.serializeSize()) + maxFee = calcMinRequiredTxRelayFee(HighFeeRate, tx.serializeSize()) return fee > maxFee @@ -3406,8 +3409,8 @@ def calcMinRequiredTxRelayFee(relayFeePerKb, txSerializeSize): pool and relayed. Args: - relayFeePerKb (float): The fee per kilobyte. - txSerializeSize int: (Size) of the byte-encoded transaction. + relayFeePerKb (int): The fee in atoms per kilobyte. + txSerializeSize (int): Size of the byte-encoded transaction. Returns: int: Fee in atoms. @@ -3436,7 +3439,7 @@ def isDustAmount(amount, scriptSize, relayFeePerKb): Args: amount (int): Atoms. scriptSize (int): Byte-size of the script. - relayFeePerKb (float): Fees paid per kilobyte. + relayFeePerKb (int): Fees paid in atoms per kilobyte. Returns: bool: True if the amount is considered dust. @@ -3495,7 +3498,7 @@ def isDustOutput(output, relayFeePerKb): Args: output (wire.TxOut): The transaction output. - relayFeePerKb: Minimum transaction fee allowable. + relayFeePerKb (int): The transaction fee in atoms per kb. Returns: bool: True if output is a dust output. @@ -3770,7 +3773,7 @@ def stakePoolTicketFee(stakeDiff, relayFee, height, poolFee, subsidyCache, netPa Args: stakeDiff (int): The ticket price. - relayFee (int): Transaction fees. + relayFee (int): Transaction fees in atoms per kb. height (int): Current block height. poolFee (int): The pools fee, as percent. subsidyCache (calc.SubsidyCache): A subsidy cache. @@ -3805,8 +3808,8 @@ def stakePoolTicketFee(stakeDiff, relayFee, height, poolFee, subsidyCache, netPa # The numerator is (p*10000*s*(v+z)) << 64. shift = 64 s = subsidy - v = int(stakeDiff) - z = int(relayFee) + v = stakeDiff + z = relayFee num = poolFeeInt num *= s vPlusZ = v + z diff --git a/decred/tests/integration/dcr/test_dcrdata_live.py b/decred/tests/integration/dcr/test_dcrdata_live.py index e051184a..25a32afc 100644 --- a/decred/tests/integration/dcr/test_dcrdata_live.py +++ b/decred/tests/integration/dcr/test_dcrdata_live.py @@ -138,7 +138,7 @@ def utxosource(amt, filter): ) ticket, spent, newUTXOs = blockchain.purchaseTickets( - KeySource(), utxosource, request + KeySource(), utxosource, request, 1e4 ) finally: blockchain.close() @@ -248,5 +248,5 @@ def __init__( internal=lambda: "", ) redeemScript = ByteArray(test.redeemScript) - revocation = blockchain.revokeTicket(ticket, keysource, redeemScript) + revocation = blockchain.revokeTicket(ticket, keysource, redeemScript, 1e4) assert test.revocation == revocation.txHex()